change application id for release

This commit is contained in:
dkanada 2020-05-09 04:43:09 +09:00
commit 9d08253655
159 changed files with 801 additions and 801 deletions

View file

@ -0,0 +1,64 @@
package com.dkanada.gramophone;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.shortcuts.DynamicShortcutManager;
import org.jellyfin.apiclient.interaction.AndroidConnectionManager;
import org.jellyfin.apiclient.interaction.AndroidDevice;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.ApiEventListener;
import org.jellyfin.apiclient.interaction.connectionmanager.ConnectionManager;
import org.jellyfin.apiclient.interaction.device.IDevice;
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.IJsonSerializer;
import org.jellyfin.apiclient.model.session.ClientCapabilities;
public class App extends Application {
private static App app;
private static ApiClient apiClient;
@Override
public void onCreate() {
super.onCreate();
app = this;
// default theme
if (!ThemeStore.isConfigured(this, 1)) {
ThemeStore.editTheme(this).primaryColorRes(R.color.md_indigo_500).accentColorRes(R.color.md_pink_A400).commit();
}
// dynamic shortcuts
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
new DynamicShortcutManager(this).initDynamicShortcuts();
}
}
public static ConnectionManager getConnectionManager(Context context, IJsonSerializer jsonSerializer, ILogger logger, IAsyncHttpClient httpClient) {
String appName = context.getString(R.string.app_name);
String appVersion = BuildConfig.VERSION_NAME;
IDevice device = new AndroidDevice(context);
ClientCapabilities capabilities = new ClientCapabilities();
ApiEventListener eventListener = new ApiEventListener();
return new AndroidConnectionManager(context, jsonSerializer, logger, httpClient, appName, appVersion, device, capabilities, eventListener);
}
public static ApiClient getApiClient() {
return apiClient;
}
public static void setApiClient(ApiClient client) {
apiClient = client;
}
public static App getInstance() {
return app;
}
}

View file

@ -0,0 +1,154 @@
package com.dkanada.gramophone.adapter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.misc.CustomFragmentStatePagerAdapter;
import com.dkanada.gramophone.model.Song;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class AlbumCoverPagerAdapter extends CustomFragmentStatePagerAdapter {
private List<Song> dataSet;
private AlbumCoverFragment.ColorReceiver currentColorReceiver;
private int currentColorReceiverPosition = -1;
public AlbumCoverPagerAdapter(FragmentManager fm, List<Song> dataSet) {
super(fm);
this.dataSet = dataSet;
}
@Override
public Fragment getItem(final int position) {
return AlbumCoverFragment.newInstance(dataSet.get(position));
}
@Override
public int getCount() {
return dataSet.size();
}
@Override
@NonNull
public Object instantiateItem(ViewGroup container, int position) {
Object o = super.instantiateItem(container, position);
if (currentColorReceiver != null && currentColorReceiverPosition == position) {
receiveColor(currentColorReceiver, currentColorReceiverPosition);
}
return o;
}
// only the latest ColorReceiver is guaranteed a response
public void receiveColor(AlbumCoverFragment.ColorReceiver colorReceiver, int position) {
AlbumCoverFragment fragment = (AlbumCoverFragment) getFragment(position);
if (fragment != null) {
currentColorReceiver = null;
currentColorReceiverPosition = -1;
fragment.receiveColor(colorReceiver, position);
} else {
currentColorReceiver = colorReceiver;
currentColorReceiverPosition = position;
}
}
public static class AlbumCoverFragment extends Fragment {
private static final String SONG_ARG = "song";
private Unbinder unbinder;
@BindView(R.id.player_image)
ImageView albumCover;
private boolean isColorReady;
private int color;
private Song song;
private ColorReceiver colorReceiver;
private int request;
public static AlbumCoverFragment newInstance(final Song song) {
AlbumCoverFragment frag = new AlbumCoverFragment();
final Bundle args = new Bundle();
args.putParcelable(SONG_ARG, song);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
song = getArguments().getParcelable(SONG_ARG);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_album_cover, container, false);
unbinder = ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
loadAlbumCover();
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
colorReceiver = null;
}
private void loadAlbumCover() {
CustomGlideRequest.Builder
.from(Glide.with(getContext()), song.primary)
.generatePalette(getActivity()).build()
.into(new CustomPaletteTarget(albumCover) {
@Override
public void onColorReady(int color) {
setColor(color);
}
});
}
private void setColor(int color) {
this.color = color;
isColorReady = true;
if (colorReceiver != null) {
colorReceiver.onColorReady(color, request);
colorReceiver = null;
}
}
public void receiveColor(ColorReceiver colorReceiver, int request) {
if (isColorReady) {
colorReceiver.onColorReady(color, request);
} else {
this.colorReceiver = colorReceiver;
this.request = request;
}
}
public interface ColorReceiver {
void onColorReady(int color, int request);
}
}
}

View file

@ -0,0 +1,112 @@
package com.dkanada.gramophone.adapter;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.CategoryInfo;
import com.dkanada.gramophone.helper.SwipeAndDragHelper;
import java.util.List;
public class CategoryInfoAdapter extends RecyclerView.Adapter<CategoryInfoAdapter.ViewHolder> implements SwipeAndDragHelper.ActionCompletionContract {
private List<CategoryInfo> categoryInfos;
private ItemTouchHelper touchHelper;
public CategoryInfoAdapter(List<CategoryInfo> categoryInfos) {
this.categoryInfos = categoryInfos;
SwipeAndDragHelper swipeAndDragHelper = new SwipeAndDragHelper(this);
touchHelper = new ItemTouchHelper(swipeAndDragHelper);
}
@Override
@NonNull
public CategoryInfoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.preference_dialog_library_categories_listitem, parent, false);
return new ViewHolder(view);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBindViewHolder(@NonNull CategoryInfoAdapter.ViewHolder holder, int position) {
CategoryInfo categoryInfo = categoryInfos.get(position);
holder.checkBox.setChecked(categoryInfo.visible);
holder.title.setText(holder.title.getResources().getString(categoryInfo.category.stringRes));
holder.itemView.setOnClickListener(v -> {
if (!(categoryInfo.visible && isLastCheckedCategory(categoryInfo))) {
categoryInfo.visible = !categoryInfo.visible;
holder.checkBox.setChecked(categoryInfo.visible);
} else {
Toast.makeText(holder.itemView.getContext(), R.string.you_have_to_select_at_least_one_category, Toast.LENGTH_SHORT).show();
}
});
holder.dragView.setOnTouchListener((view, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(holder);
}
return false;
}
);
}
@Override
public int getItemCount() {
return categoryInfos.size();
}
public void setCategoryInfos(List<CategoryInfo> categoryInfos) {
this.categoryInfos = categoryInfos;
notifyDataSetChanged();
}
@Override
public void onViewMoved(int oldPosition, int newPosition) {
CategoryInfo categoryInfo = categoryInfos.get(oldPosition);
categoryInfos.remove(oldPosition);
categoryInfos.add(newPosition, categoryInfo);
notifyItemMoved(oldPosition, newPosition);
}
public void attachToRecyclerView(RecyclerView recyclerView) {
touchHelper.attachToRecyclerView(recyclerView);
}
public List<CategoryInfo> getCategoryInfos() {
return categoryInfos;
}
private boolean isLastCheckedCategory(CategoryInfo categoryInfo) {
if (categoryInfo.visible) {
for (CategoryInfo c : categoryInfos) {
if (c != categoryInfo && c.visible) return false;
}
}
return true;
}
static class ViewHolder extends RecyclerView.ViewHolder {
public CheckBox checkBox;
public TextView title;
public View dragView;
public ViewHolder(View view) {
super(view);
checkBox = view.findViewById(R.id.checkbox);
title = view.findViewById(R.id.title);
dragView = view.findViewById(R.id.drag_view);
}
}
}

View file

@ -0,0 +1,123 @@
package com.dkanada.gramophone.adapter;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.model.Genre;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import java.util.List;
public class GenreAdapter extends RecyclerView.Adapter<GenreAdapter.ViewHolder> implements FastScrollRecyclerView.SectionedAdapter {
private final AppCompatActivity activity;
private List<Genre> dataSet;
private int itemLayoutRes;
public GenreAdapter(@NonNull AppCompatActivity activity, List<Genre> dataSet, @LayoutRes int itemLayoutRes) {
this.activity = activity;
this.dataSet = dataSet;
this.itemLayoutRes = itemLayoutRes;
}
public List<Genre> getDataSet() {
return dataSet;
}
public void swapDataSet(List<Genre> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return dataSet.get(position).hashCode();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
final Genre genre = dataSet.get(position);
if (holder.getAdapterPosition() == getItemCount() - 1) {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.VISIBLE);
}
}
if (holder.menu != null) {
holder.menu.setVisibility(View.GONE);
}
if (holder.title != null) {
holder.title.setText(genre.name);
}
loadImage(genre, holder);
}
protected void loadImage(Genre genre, final GenreAdapter.ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), genre.id)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
}
@Override
public void onColorReady(int color) {
}
});
}
@Override
public int getItemCount() {
return dataSet.size();
}
@NonNull
@Override
public String getSectionName(int position) {
final Genre genre = dataSet.get(position);
return genre.id.hashCode() == -1 ? "" : MusicUtil.getSectionName(dataSet.get(position).name);
}
public class ViewHolder extends MediaEntryViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
public void onClick(View view) {
Genre genre = dataSet.get(getAdapterPosition());
NavigationUtil.goToGenre(activity, genre);
}
}
}

View file

@ -0,0 +1,189 @@
package com.dkanada.gramophone.adapter;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.dkanada.gramophone.model.CategoryInfo;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.pager.AlbumsFragment;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.pager.ArtistsFragment;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.pager.GenresFragment;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.pager.PlaylistsFragment;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.pager.SongsFragment;
import com.dkanada.gramophone.util.PreferenceUtil;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
private final SparseArray<WeakReference<Fragment>> mFragmentArray = new SparseArray<>();
private final List<Holder> mHolderList = new ArrayList<>();
@NonNull
private final Context mContext;
public MusicLibraryPagerAdapter(@NonNull final Context context, final FragmentManager fragmentManager) {
super(fragmentManager);
mContext = context;
setCategoryInfos(PreferenceUtil.getInstance(context).getLibraryCategories());
}
public void setCategoryInfos(@NonNull List<CategoryInfo> categoryInfos) {
mHolderList.clear();
for (CategoryInfo categoryInfo : categoryInfos) {
if (categoryInfo.visible) {
MusicFragments fragment = MusicFragments.valueOf(categoryInfo.category.toString());
Holder holder = new Holder();
holder.mClassName = fragment.getFragmentClass().getName();
holder.title = mContext.getResources()
.getString(categoryInfo.category.stringRes)
.toUpperCase(Locale.getDefault());
mHolderList.add(holder);
}
}
alignCache();
notifyDataSetChanged();
}
public Fragment getFragment(final int position) {
final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
if (mWeakFragment != null && mWeakFragment.get() != null) {
return mWeakFragment.get();
}
return getItem(position);
}
@Override
public int getItemPosition(@NonNull Object fragment) {
for (int i = 0, size = mHolderList.size(); i < size; i++) {
Holder holder = mHolderList.get(i);
if (holder.mClassName.equals(fragment.getClass().getName())) {
return i;
}
}
return POSITION_NONE;
}
@Override
public long getItemId(int position) {
// as fragment position is not fixed, we can't use position as id
return MusicFragments.of(getFragment(position).getClass()).ordinal();
}
@NonNull
@Override
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
final Fragment mFragment = (Fragment) super.instantiateItem(container, position);
final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
if (mWeakFragment != null) {
mWeakFragment.clear();
}
mFragmentArray.put(position, new WeakReference<>(mFragment));
return mFragment;
}
@Override
public Fragment getItem(final int position) {
final Holder mCurrentHolder = mHolderList.get(position);
return Fragment.instantiate(mContext, mCurrentHolder.mClassName, mCurrentHolder.mParams);
}
@Override
public void destroyItem(final ViewGroup container, final int position, final Object object) {
super.destroyItem(container, position, object);
final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
if (mWeakFragment != null) {
mWeakFragment.clear();
}
}
@Override
public int getCount() {
return mHolderList.size();
}
@NonNull
@Override
public CharSequence getPageTitle(final int position) {
return mHolderList.get(position).title;
}
/**
* Aligns the fragment cache with the current category layout.
*/
private void alignCache() {
if (mFragmentArray.size() == 0) return;
HashMap<String, WeakReference<Fragment>> mappings = new HashMap<>(mFragmentArray.size());
for (int i = 0, size = mFragmentArray.size(); i < size; i++) {
WeakReference<Fragment> ref = mFragmentArray.valueAt(i);
Fragment fragment = ref.get();
if (fragment != null) {
mappings.put(fragment.getClass().getName(), ref);
}
}
for (int i = 0, size = mHolderList.size(); i < size; i++) {
WeakReference<Fragment> ref = mappings.get(mHolderList.get(i).mClassName);
if (ref != null) {
mFragmentArray.put(i, ref);
} else {
mFragmentArray.remove(i);
}
}
}
public enum MusicFragments {
SONGS(SongsFragment.class),
ALBUMS(AlbumsFragment.class),
ARTISTS(ArtistsFragment.class),
GENRES(GenresFragment.class),
PLAYLISTS(PlaylistsFragment.class);
private final Class<? extends Fragment> mFragmentClass;
MusicFragments(final Class<? extends Fragment> fragmentClass) {
mFragmentClass = fragmentClass;
}
public Class<? extends Fragment> getFragmentClass() {
return mFragmentClass;
}
public static MusicFragments of(Class<?> cl) {
MusicFragments[] fragments = All.FRAGMENTS;
for (MusicFragments fragment : fragments) {
if (cl.equals(fragment.mFragmentClass)) {
return fragment;
}
}
throw new IllegalArgumentException("Unknown music fragment " + cl);
}
private static class All {
public static final MusicFragments[] FRAGMENTS = values();
}
}
private final static class Holder {
String mClassName;
Bundle mParams;
String title;
}
}

View file

@ -0,0 +1,175 @@
package com.dkanada.gramophone.adapter;
import android.graphics.drawable.Drawable;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import com.bumptech.glide.Glide;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.base.AbsMultiSelectAdapter;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.dialogs.DeletePlaylistDialog;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.helper.menu.PlaylistMenuHelper;
import com.dkanada.gramophone.helper.menu.SongsMenuHelper;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.NavigationUtil;
import java.util.ArrayList;
import java.util.List;
public class PlaylistAdapter extends AbsMultiSelectAdapter<PlaylistAdapter.ViewHolder, Playlist> {
protected final AppCompatActivity activity;
protected List<Playlist> dataSet;
protected int itemLayoutRes;
public PlaylistAdapter(AppCompatActivity activity, List<Playlist> dataSet, @LayoutRes int itemLayoutRes, @Nullable CabHolder cabHolder) {
super(activity, cabHolder, R.menu.menu_playlists_selection);
this.activity = activity;
this.dataSet = dataSet;
this.itemLayoutRes = itemLayoutRes;
setHasStableIds(true);
}
public List<Playlist> getDataSet() {
return dataSet;
}
public void swapDataSet(List<Playlist> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return dataSet.get(position).hashCode();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false);
return createViewHolder(view, viewType);
}
protected ViewHolder createViewHolder(View view, int viewType) {
return new ViewHolder(view, viewType);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
final Playlist playlist = dataSet.get(position);
holder.itemView.setActivated(isChecked(playlist));
if (holder.title != null) {
holder.title.setText(playlist.name);
}
if (holder.getAdapterPosition() == getItemCount() - 1) {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.VISIBLE);
}
}
loadImage(playlist, holder);
}
protected void loadImage(Playlist playlist, final PlaylistAdapter.ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), playlist.id)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
}
@Override
public void onColorReady(int color) {
}
});
}
@Override
public int getItemCount() {
return dataSet.size();
}
@Override
protected Playlist getIdentifier(int position) {
return dataSet.get(position);
}
@Override
protected String getName(Playlist playlist) {
return playlist.name;
}
@Override
protected void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Playlist> selection) {
switch (menuItem.getItemId()) {
case R.id.action_delete_playlist:
DeletePlaylistDialog.create(selection).show(activity.getSupportFragmentManager(), "DELETE_PLAYLIST");
break;
default:
SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.getItemId());
break;
}
}
@NonNull
private List<Song> getSongList(@NonNull List<Playlist> playlists) {
final List<Song> songs = new ArrayList<>();
return songs;
}
public class ViewHolder extends MediaEntryViewHolder {
public ViewHolder(@NonNull View itemView, int itemViewType) {
super(itemView);
if (menu != null) {
menu.setOnClickListener(view -> {
final PopupMenu popupMenu = new PopupMenu(activity, view);
popupMenu.inflate(R.menu.menu_item_playlist);
popupMenu.setOnMenuItemClickListener(item -> PlaylistMenuHelper.handleMenuClick(activity, dataSet.get(getAdapterPosition()), item));
popupMenu.show();
});
}
}
@Override
public void onClick(View view) {
if (isInQuickSelectMode()) {
toggleChecked(getAdapterPosition());
} else {
Playlist playlist = dataSet.get(getAdapterPosition());
NavigationUtil.goToPlaylist(activity, playlist);
}
}
@Override
public boolean onLongClick(View view) {
toggleChecked(getAdapterPosition());
return true;
}
}
}

View file

@ -0,0 +1,167 @@
package com.dkanada.gramophone.adapter;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.util.ATHUtil;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.helper.menu.SongMenuHelper;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import java.util.ArrayList;
import java.util.List;
public class SearchAdapter extends RecyclerView.Adapter<SearchAdapter.ViewHolder> {
private static final int HEADER = 0;
private static final int ALBUM = 1;
private static final int ARTIST = 2;
private static final int SONG = 3;
private final AppCompatActivity activity;
private List<Object> dataSet;
public SearchAdapter(@NonNull AppCompatActivity activity, @NonNull List<Object> dataSet) {
this.activity = activity;
this.dataSet = dataSet;
}
public void swapDataSet(@NonNull List<Object> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if (dataSet.get(position) instanceof Album) return ALBUM;
if (dataSet.get(position) instanceof Artist) return ARTIST;
if (dataSet.get(position) instanceof Song) return SONG;
return HEADER;
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == HEADER)
return new ViewHolder(LayoutInflater.from(activity).inflate(R.layout.sub_header, parent, false), viewType);
return new ViewHolder(LayoutInflater.from(activity).inflate(R.layout.item_list, parent, false), viewType);
}
@SuppressWarnings("ConstantConditions")
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case ALBUM:
final Album album = (Album) dataSet.get(position);
holder.title.setText(album.getTitle());
holder.text.setText(MusicUtil.getAlbumInfoString(activity, album));
CustomGlideRequest.Builder
.from(Glide.with(activity), album.primary)
.build().into(holder.image);
break;
case ARTIST:
final Artist artist = (Artist) dataSet.get(position);
holder.title.setText(artist.getName());
holder.text.setText(MusicUtil.getArtistInfoString(activity, artist));
CustomGlideRequest.Builder
.from(Glide.with(activity), artist.primary)
.build().into(holder.image);
break;
case SONG:
final Song song = (Song) dataSet.get(position);
holder.title.setText(song.title);
holder.text.setText(MusicUtil.getSongInfoString(song));
break;
default:
holder.title.setText(dataSet.get(position).toString());
break;
}
}
@Override
public int getItemCount() {
return dataSet.size();
}
public class ViewHolder extends MediaEntryViewHolder {
public ViewHolder(@NonNull View itemView, int itemViewType) {
super(itemView);
itemView.setOnLongClickListener(null);
if (itemViewType != HEADER) {
itemView.setBackgroundColor(ATHUtil.resolveColor(activity, R.attr.cardBackgroundColor));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
itemView.setElevation(activity.getResources().getDimensionPixelSize(R.dimen.card_elevation));
}
if (shortSeparator != null) {
shortSeparator.setVisibility(View.GONE);
}
}
if (menu != null) {
if (itemViewType == SONG) {
menu.setVisibility(View.VISIBLE);
menu.setOnClickListener(new SongMenuHelper.OnClickSongMenu(activity) {
@Override
public Song getSong() {
return (Song) dataSet.get(getAdapterPosition());
}
});
} else {
menu.setVisibility(View.GONE);
}
}
switch (itemViewType) {
case ALBUM:
setImageTransitionName(activity.getString(R.string.transition_album_art));
break;
case ARTIST:
setImageTransitionName(activity.getString(R.string.transition_artist_image));
break;
default:
View container = itemView.findViewById(R.id.image_container);
if (container != null) {
container.setVisibility(View.GONE);
}
break;
}
}
@Override
public void onClick(View view) {
Object item = dataSet.get(getAdapterPosition());
switch (getItemViewType()) {
case ALBUM:
NavigationUtil.goToAlbum(activity,
(Album) item,
Pair.create(image, activity.getResources().getString(R.string.transition_album_art)));
break;
case ARTIST:
NavigationUtil.goToArtist(activity,
(Artist) item,
Pair.create(image, activity.getResources().getString(R.string.transition_artist_image)));
break;
case SONG:
List<Song> playList = new ArrayList<>();
playList.add((Song) item);
MusicPlayerRemote.openQueue(playList, 0, true);
break;
}
}
}
}

View file

@ -0,0 +1,232 @@
package com.dkanada.gramophone.adapter.album;
import android.graphics.drawable.Drawable;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.base.AbsMultiSelectAdapter;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.helper.menu.SongsMenuHelper;
import com.dkanada.gramophone.helper.sort.SortMethod;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import java.util.ArrayList;
import java.util.List;
public class AlbumAdapter extends AbsMultiSelectAdapter<AlbumAdapter.ViewHolder, Album> implements FastScrollRecyclerView.SectionedAdapter {
protected final AppCompatActivity activity;
protected List<Album> dataSet;
protected int itemLayoutRes;
protected boolean usePalette;
public AlbumAdapter(@NonNull AppCompatActivity activity, List<Album> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, cabHolder, R.menu.menu_media_selection);
this.activity = activity;
this.dataSet = dataSet;
this.itemLayoutRes = itemLayoutRes;
this.usePalette = usePalette;
setHasStableIds(true);
}
public void usePalette(boolean usePalette) {
this.usePalette = usePalette;
notifyDataSetChanged();
}
public void swapDataSet(List<Album> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
public List<Album> getDataSet() {
return dataSet;
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false);
return createViewHolder(view, viewType);
}
protected ViewHolder createViewHolder(View view, int viewType) {
return new ViewHolder(view);
}
protected String getAlbumTitle(Album album) {
return album.getTitle();
}
protected String getAlbumText(Album album) {
return MusicUtil.getAlbumInfoString(activity, album);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
final Album album = dataSet.get(position);
final boolean isChecked = isChecked(album);
holder.itemView.setActivated(isChecked);
if (holder.getAdapterPosition() == getItemCount() - 1) {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.VISIBLE);
}
}
if (holder.title != null) {
holder.title.setText(getAlbumTitle(album));
}
if (holder.text != null) {
holder.text.setText(getAlbumText(album));
}
loadAlbumCover(album, holder);
}
protected void setColors(int color, ViewHolder holder) {
if (holder.paletteColorContainer != null) {
holder.paletteColorContainer.setBackgroundColor(color);
if (holder.title != null) {
holder.title.setTextColor(MaterialValueHelper.getPrimaryTextColor(activity, ColorUtil.isColorLight(color)));
}
if (holder.text != null) {
holder.text.setTextColor(MaterialValueHelper.getSecondaryTextColor(activity, ColorUtil.isColorLight(color)));
}
}
}
protected void loadAlbumCover(Album album, final ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), album.primary)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
setColors(getDefaultFooterColor(), holder);
}
@Override
public void onColorReady(int color) {
if (usePalette) {
setColors(color, holder);
} else {
setColors(getDefaultFooterColor(), holder);
}
}
});
}
@Override
public int getItemCount() {
return dataSet.size();
}
@Override
public long getItemId(int position) {
return dataSet.get(position).hashCode();
}
@Override
protected Album getIdentifier(int position) {
return dataSet.get(position);
}
@Override
protected String getName(Album album) {
return album.getTitle();
}
@Override
protected void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Album> selection) {
SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.getItemId());
}
@NonNull
private List<Song> getSongList(@NonNull List<Album> albums) {
final List<Song> songs = new ArrayList<>();
for (Album album : albums) {
songs.addAll(album.songs);
}
return songs;
}
@NonNull
@Override
public String getSectionName(int position) {
@Nullable String sectionName = null;
switch (PreferenceUtil.getInstance(activity).getAlbumSortMethod()) {
case SortMethod.NAME:
sectionName = dataSet.get(position).getTitle();
break;
case SortMethod.ARTIST:
sectionName = dataSet.get(position).getArtistName();
break;
case SortMethod.YEAR:
return MusicUtil.getYearString(dataSet.get(position).getYear());
case SortMethod.RANDOM:
return activity.getResources().getString(R.string.random);
}
return MusicUtil.getSectionName(sectionName);
}
public class ViewHolder extends MediaEntryViewHolder {
public ViewHolder(@NonNull final View itemView) {
super(itemView);
setImageTransitionName(activity.getString(R.string.transition_album_art));
if (menu != null) {
menu.setVisibility(View.GONE);
}
}
@Override
public void onClick(View v) {
if (isInQuickSelectMode()) {
toggleChecked(getAdapterPosition());
} else {
Pair[] albumPairs = new Pair[]{Pair.create(image, activity.getResources().getString(R.string.transition_album_art))};
NavigationUtil.goToAlbum(activity, dataSet.get(getAdapterPosition()), albumPairs);
}
}
@Override
public boolean onLongClick(View view) {
toggleChecked(getAdapterPosition());
return true;
}
}
}

View file

@ -0,0 +1,84 @@
package com.dkanada.gramophone.adapter.album;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.helper.HorizontalAdapterHelper;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.util.MusicUtil;
import java.util.List;
public class HorizontalAlbumAdapter extends AlbumAdapter {
public HorizontalAlbumAdapter(@NonNull AppCompatActivity activity, List<Album> dataSet, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, usePalette, cabHolder);
}
@Override
protected ViewHolder createViewHolder(View view, int viewType) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
HorizontalAdapterHelper.applyMarginToLayoutParams(activity, params, viewType);
return new ViewHolder(view);
}
@Override
protected void setColors(int color, ViewHolder holder) {
if (holder.itemView != null) {
CardView card=(CardView)holder.itemView;
card.setCardBackgroundColor(color);
if (holder.title != null) {
holder.title.setTextColor(MaterialValueHelper.getPrimaryTextColor(activity, ColorUtil.isColorLight(color)));
}
if (holder.text != null) {
holder.text.setTextColor(MaterialValueHelper.getSecondaryTextColor(activity, ColorUtil.isColorLight(color)));
}
}
}
@Override
protected void loadAlbumCover(Album album, final ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), album.primary)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
setColors(getAlbumArtistFooterColor(), holder);
}
@Override
public void onColorReady(int color) {
if (usePalette) {
setColors(color, holder);
} else {
setColors(getAlbumArtistFooterColor(), holder);
}
}
});
}
@Override
protected String getAlbumText(Album album) {
return MusicUtil.getYearString(album.getYear());
}
@Override
public int getItemViewType(int position) {
return HorizontalAdapterHelper.getItemViewtype(position, getItemCount());
}
}

View file

@ -0,0 +1,209 @@
package com.dkanada.gramophone.adapter.artist;
import android.graphics.drawable.Drawable;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.base.AbsMultiSelectAdapter;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.helper.menu.SongsMenuHelper;
import com.dkanada.gramophone.helper.sort.SortMethod;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import java.util.ArrayList;
import java.util.List;
public class ArtistAdapter extends AbsMultiSelectAdapter<ArtistAdapter.ViewHolder, Artist> implements FastScrollRecyclerView.SectionedAdapter {
protected final AppCompatActivity activity;
protected List<Artist> dataSet;
protected int itemLayoutRes;
protected boolean usePalette;
public ArtistAdapter(@NonNull AppCompatActivity activity, List<Artist> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, cabHolder, R.menu.menu_media_selection);
this.activity = activity;
this.dataSet = dataSet;
this.itemLayoutRes = itemLayoutRes;
this.usePalette = usePalette;
setHasStableIds(true);
}
public void swapDataSet(List<Artist> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
public List<Artist> getDataSet() {
return dataSet;
}
public void usePalette(boolean usePalette) {
this.usePalette = usePalette;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return dataSet.get(position).hashCode();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false);
return createViewHolder(view);
}
protected ViewHolder createViewHolder(View view) {
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
final Artist artist = dataSet.get(position);
boolean isChecked = isChecked(artist);
holder.itemView.setActivated(isChecked);
if (holder.getAdapterPosition() == getItemCount() - 1) {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.VISIBLE);
}
}
if (holder.title != null) {
holder.title.setText(artist.getName());
}
if (holder.text != null) {
holder.text.setText(MusicUtil.getArtistInfoString(activity, artist));
}
holder.itemView.setActivated(isChecked(artist));
loadArtistImage(artist, holder);
}
private void setColors(int color, ViewHolder holder) {
if (holder.paletteColorContainer != null) {
holder.paletteColorContainer.setBackgroundColor(color);
if (holder.title != null) {
holder.title.setTextColor(MaterialValueHelper.getPrimaryTextColor(activity, ColorUtil.isColorLight(color)));
}
if (holder.text != null) {
holder.text.setTextColor(MaterialValueHelper.getSecondaryTextColor(activity, ColorUtil.isColorLight(color)));
}
}
}
protected void loadArtistImage(Artist artist, final ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), artist.primary)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
setColors(getDefaultFooterColor(), holder);
}
@Override
public void onColorReady(int color) {
if (usePalette) {
setColors(color, holder);
} else {
setColors(getDefaultFooterColor(), holder);
}
}
});
}
@Override
public int getItemCount() {
return dataSet.size();
}
@Override
protected Artist getIdentifier(int position) {
return dataSet.get(position);
}
@Override
protected String getName(Artist artist) {
return artist.getName();
}
@Override
protected void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Artist> selection) {
SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.getItemId());
}
@NonNull
private List<Song> getSongList(@NonNull List<Artist> artists) {
final List<Song> songs = new ArrayList<>();
for (Artist artist : artists) {
songs.addAll(artist.getSongs());
}
return songs;
}
@NonNull
@Override
public String getSectionName(int position) {
return MusicUtil.getSectionName(dataSet.get(position).getName());
}
public class ViewHolder extends MediaEntryViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
setImageTransitionName(activity.getString(R.string.transition_artist_image));
if (menu != null) {
menu.setVisibility(View.GONE);
}
}
@Override
public void onClick(View v) {
if (isInQuickSelectMode()) {
toggleChecked(getAdapterPosition());
} else {
Pair[] artistPairs = new Pair[]{Pair.create(image, activity.getResources().getString(R.string.transition_artist_image))};
NavigationUtil.goToArtist(activity, dataSet.get(getAdapterPosition()), artistPairs);
}
}
@Override
public boolean onLongClick(View view) {
toggleChecked(getAdapterPosition());
return true;
}
}
}

View file

@ -0,0 +1,125 @@
package com.dkanada.gramophone.adapter.base;
import android.content.Context;
import androidx.annotation.MenuRes;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import com.afollestad.materialcab.MaterialCab;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.interfaces.CabHolder;
import java.util.ArrayList;
import java.util.List;
public abstract class AbsMultiSelectAdapter<VH extends RecyclerView.ViewHolder, I> extends RecyclerView.Adapter<VH> implements MaterialCab.Callback {
private final Context context;
private final CabHolder cabHolder;
private int menuRes;
private MaterialCab cab;
private List<I> checked;
public AbsMultiSelectAdapter(Context context, @Nullable CabHolder cabHolder, @MenuRes int menuRes) {
this.context = context;
this.cabHolder = cabHolder;
this.menuRes = menuRes;
this.checked = new ArrayList<>();
}
protected void setMultiSelectMenuRes(@MenuRes int menuRes) {
this.menuRes = menuRes;
}
protected boolean toggleChecked(final int position) {
if (cabHolder != null) {
I identifier = getIdentifier(position);
if (identifier == null) return false;
if (!checked.remove(identifier)) checked.add(identifier);
notifyItemChanged(position);
updateCab();
return true;
}
return false;
}
protected void checkAll() {
if (cabHolder != null) {
checked.clear();
for (int i = 0; i < getItemCount(); i++) {
I identifier = getIdentifier(i);
if (identifier != null) {
checked.add(identifier);
}
}
notifyDataSetChanged();
updateCab();
}
}
private void updateCab() {
if (cabHolder != null) {
if (cab == null || !cab.isActive()) {
cab = cabHolder.openCab(menuRes, this);
}
final int size = checked.size();
if (size <= 0) cab.finish();
else if (size == 1) cab.setTitle(getName(checked.get(0)));
else cab.setTitle(context.getString(R.string.x_selected, size));
}
}
private void clearChecked() {
checked.clear();
notifyDataSetChanged();
}
protected boolean isChecked(I identifier) {
return checked.contains(identifier);
}
protected boolean isInQuickSelectMode() {
return cab != null && cab.isActive();
}
@Override
public boolean onCabCreated(MaterialCab materialCab, Menu menu) {
return true;
}
@Override
public boolean onCabItemClicked(MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_multi_select_adapter_check_all) {
checkAll();
} else {
onMultipleItemAction(menuItem, new ArrayList<>(checked));
cab.finish();
clearChecked();
}
return true;
}
@Override
public boolean onCabFinished(MaterialCab materialCab) {
clearChecked();
return true;
}
protected String getName(I object) {
return object.toString();
}
@Nullable
protected abstract I getIdentifier(int position);
protected abstract void onMultipleItemAction(MenuItem menuItem, List<I> selection);
}

View file

@ -0,0 +1,75 @@
package com.dkanada.gramophone.adapter.base;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.dkanada.gramophone.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MediaEntryViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
@Nullable
@BindView(R.id.image)
public ImageView image;
@Nullable
@BindView(R.id.image_text)
public TextView imageText;
@Nullable
@BindView(R.id.title)
public TextView title;
@Nullable
@BindView(R.id.text)
public TextView text;
@Nullable
@BindView(R.id.menu)
public View menu;
@Nullable
@BindView(R.id.separator)
public View separator;
@Nullable
@BindView(R.id.short_separator)
public View shortSeparator;
@Nullable
@BindView(R.id.drag_view)
public View dragView;
@Nullable
@BindView(R.id.palette_color_container)
public View paletteColorContainer;
public MediaEntryViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
protected void setImageTransitionName(@NonNull String transitionName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && image != null) {
image.setTransitionName(transitionName);
}
}
@Override
public boolean onLongClick(View v) {
return false;
}
@Override
public void onClick(View v) {
}
}

View file

@ -0,0 +1,112 @@
package com.dkanada.gramophone.adapter.song;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Song;
import java.util.List;
public abstract class AbsOffsetSongAdapter extends SongAdapter {
protected static final int OFFSET_ITEM = 0;
protected static final int SONG = 1;
public AbsOffsetSongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder);
}
public AbsOffsetSongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder, boolean showSectionName) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder, showSectionName);
}
@NonNull
@Override
public SongAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == OFFSET_ITEM) {
View view = LayoutInflater.from(activity).inflate(R.layout.item_list_single_row, parent, false);
return createViewHolder(view);
}
return super.onCreateViewHolder(parent, viewType);
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new AbsOffsetSongAdapter.ViewHolder(view);
}
@Override
public long getItemId(int position) {
position--;
if (position < 0) return -2;
return super.getItemId(position);
}
@Nullable
@Override
protected Song getIdentifier(int position) {
position--;
if (position < 0) return null;
return super.getIdentifier(position);
}
@Override
public int getItemCount() {
int superItemCount = super.getItemCount();
return superItemCount == 0 ? 0 : superItemCount + 1;
}
@Override
public int getItemViewType(int position) {
return position == 0 ? OFFSET_ITEM : SONG;
}
@NonNull
@Override
public String getSectionName(int position) {
position--;
if (position < 0) return "";
return super.getSectionName(position);
}
public class ViewHolder extends SongAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
protected Song getSong() {
// return empty song just to be safe
if (getItemViewType() == OFFSET_ITEM) return Song.EMPTY_SONG;
return dataSet.get(getAdapterPosition() - 1);
}
@Override
public void onClick(View v) {
if (isInQuickSelectMode() && getItemViewType() != OFFSET_ITEM) {
toggleChecked(getAdapterPosition());
} else {
MusicPlayerRemote.openQueue(dataSet, getAdapterPosition() - 1, true);
}
}
@Override
public boolean onLongClick(View view) {
if (getItemViewType() == OFFSET_ITEM) return false;
toggleChecked(getAdapterPosition());
return true;
}
}
}

View file

@ -0,0 +1,61 @@
package com.dkanada.gramophone.adapter.song;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import java.util.List;
public class AlbumSongAdapter extends SongAdapter {
public AlbumSongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder);
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SongAdapter.ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
final Song song = dataSet.get(position);
if (holder.imageText != null) {
final int trackNumber = MusicUtil.getFixedTrackNumber(song.trackNumber);
final String trackNumberString = trackNumber > 0 ? String.valueOf(trackNumber) : "-";
holder.imageText.setText(trackNumberString);
}
}
@Override
protected String getSongText(Song song) {
return MusicUtil.getReadableDurationString(song.duration);
}
public class ViewHolder extends SongAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
if (imageText != null) {
imageText.setVisibility(View.VISIBLE);
}
if (image != null) {
image.setVisibility(View.GONE);
}
}
}
@Override
protected void loadAlbumCover(Song song, SongAdapter.ViewHolder holder) {
// don't want to load it in this adapter
}
}

View file

@ -0,0 +1,188 @@
package com.dkanada.gramophone.adapter.song;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.afollestad.materialcab.MaterialCab;
import com.bumptech.glide.Glide;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.helper.menu.SongMenuHelper;
import com.dkanada.gramophone.helper.menu.SongsMenuHelper;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.NavigationUtil;
import java.util.ArrayList;
import java.util.List;
public class ArtistSongAdapter extends ArrayAdapter<Song> implements MaterialCab.Callback {
@Nullable
private final CabHolder cabHolder;
private MaterialCab cab;
private List<Song> dataSet;
private List<Song> checked;
@NonNull
private final AppCompatActivity activity;
public ArtistSongAdapter(@NonNull AppCompatActivity activity, @NonNull List<Song> dataSet, @Nullable CabHolder cabHolder) {
super(activity, R.layout.item_list, dataSet);
this.activity = activity;
this.cabHolder = cabHolder;
this.dataSet = dataSet;
checked = new ArrayList<>();
}
public List<Song> getDataSet() {
return dataSet;
}
public void swapDataSet(List<Song> dataSet) {
this.dataSet = dataSet;
clear();
addAll(dataSet);
notifyDataSetChanged();
}
@Override
@NonNull
public View getView(final int position, View convertView, @NonNull ViewGroup parent) {
final Song song = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_list, parent, false);
}
final TextView songTitle = convertView.findViewById(R.id.title);
final TextView songInfo = convertView.findViewById(R.id.text);
final ImageView albumArt = convertView.findViewById(R.id.image);
final View shortSeparator = convertView.findViewById(R.id.short_separator);
if (position == getCount() - 1) {
if (shortSeparator != null) {
shortSeparator.setVisibility(View.GONE);
}
} else {
if (shortSeparator != null) {
shortSeparator.setVisibility(View.VISIBLE);
}
}
songTitle.setText(song.title);
songInfo.setText(song.albumName);
CustomGlideRequest.Builder
.from(Glide.with(activity), song.primary)
.build().into(albumArt);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
albumArt.setTransitionName(activity.getString(R.string.transition_album_art));
}
final ImageView overflowButton = convertView.findViewById(R.id.menu);
overflowButton.setOnClickListener(new SongMenuHelper.OnClickSongMenu(activity) {
@Override
public Song getSong() {
return song;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.action_go_to_album) {
Pair[] albumPairs = new Pair[]{Pair.create(albumArt, activity.getResources().getString(R.string.transition_album_art))};
NavigationUtil.goToAlbum(activity, new Album(song), albumPairs);
return true;
}
return super.onMenuItemClick(item);
}
});
convertView.setActivated(isChecked(song));
convertView.setOnClickListener(view -> {
if (isInQuickSelectMode()) {
toggleChecked(song);
} else {
MusicPlayerRemote.openQueue(dataSet, position, true);
}
});
convertView.setOnLongClickListener(view -> {
toggleChecked(song);
return true;
});
return convertView;
}
private void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Song> selection) {
SongsMenuHelper.handleMenuClick(activity, selection, menuItem.getItemId());
}
protected void toggleChecked(Song song) {
if (cabHolder != null) {
openCabIfNecessary();
if (!checked.remove(song)) checked.add(song);
notifyDataSetChanged();
final int size = checked.size();
if (size <= 0) cab.finish();
else if (size == 1) cab.setTitle(checked.get(0).title);
else if (size > 1) cab.setTitle(String.valueOf(size));
}
}
private void openCabIfNecessary() {
if (cabHolder != null) {
if (cab == null || !cab.isActive()) {
cab = cabHolder.openCab(R.menu.menu_media_selection, this);
}
}
}
private void unCheckAll() {
checked.clear();
notifyDataSetChanged();
}
protected boolean isChecked(Song song) {
return checked.contains(song);
}
protected boolean isInQuickSelectMode() {
return cab != null && cab.isActive();
}
@Override
public boolean onCabCreated(MaterialCab materialCab, Menu menu) {
return true;
}
@Override
public boolean onCabItemClicked(@NonNull MenuItem menuItem) {
onMultipleItemAction(menuItem, new ArrayList<>(checked));
cab.finish();
unCheckAll();
return true;
}
@Override
public boolean onCabFinished(MaterialCab materialCab) {
unCheckAll();
return true;
}
}

View file

@ -0,0 +1,135 @@
package com.dkanada.gramophone.adapter.song;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.annotation.DraggableItemStateFlags;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.dialogs.RemoveFromPlaylistDialog;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.ViewUtil;
import java.util.List;
public class OrderablePlaylistSongAdapter extends PlaylistSongAdapter implements DraggableItemAdapter<OrderablePlaylistSongAdapter.ViewHolder> {
private OnMoveItemListener onMoveItemListener;
public OrderablePlaylistSongAdapter(@NonNull AppCompatActivity activity, @NonNull List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder, @Nullable OnMoveItemListener onMoveItemListener) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder);
setMultiSelectMenuRes(R.menu.menu_playlists_songs_selection);
this.onMoveItemListener = onMoveItemListener;
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new OrderablePlaylistSongAdapter.ViewHolder(view);
}
@Override
public long getItemId(int position) {
position--;
if (position < 0) return -2;
return dataSet.get(position).id.hashCode();
}
@Override
protected void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Song> selection) {
switch (menuItem.getItemId()) {
case R.id.action_remove_from_playlist:
RemoveFromPlaylistDialog.create(selection).show(activity.getSupportFragmentManager(), "ADD_PLAYLIST");
return;
}
super.onMultipleItemAction(menuItem, selection);
}
@Override
public boolean onCheckCanStartDrag(ViewHolder holder, int position, int x, int y) {
return onMoveItemListener != null && position > 0 &&
(ViewUtil.hitTest(holder.dragView, x, y) || ViewUtil.hitTest(holder.image, x, y));
}
@Override
public ItemDraggableRange onGetItemDraggableRange(ViewHolder holder, int position) {
return new ItemDraggableRange(1, dataSet.size());
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
if (onMoveItemListener != null && fromPosition != toPosition) {
onMoveItemListener.onMoveItem(fromPosition - 1, toPosition - 1);
}
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return dropPosition > 0;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
public interface OnMoveItemListener {
void onMoveItem(int fromPosition, int toPosition);
}
public class ViewHolder extends PlaylistSongAdapter.ViewHolder implements DraggableItemViewHolder {
@DraggableItemStateFlags
private int mDragStateFlags;
public ViewHolder(@NonNull View itemView) {
super(itemView);
if (dragView != null) {
if (onMoveItemListener != null) {
dragView.setVisibility(View.VISIBLE);
} else {
dragView.setVisibility(View.GONE);
}
}
}
@Override
protected int getSongMenuRes() {
return R.menu.menu_item_playlist_song;
}
@Override
protected boolean onSongMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_remove_from_playlist:
RemoveFromPlaylistDialog.create(getSong()).show(activity.getSupportFragmentManager(), "REMOVE_FROM_PLAYLIST");
return true;
}
return super.onSongMenuItemClick(item);
}
@Override
public void setDragStateFlags(@DraggableItemStateFlags int flags) {
mDragStateFlags = flags;
}
@Override
@DraggableItemStateFlags
public int getDragStateFlags() {
return mDragStateFlags;
}
}
}

View file

@ -0,0 +1,170 @@
package com.dkanada.gramophone.adapter.song;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.annotation.DraggableItemStateFlags;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.ViewUtil;
import java.util.List;
public class PlayingQueueAdapter extends SongAdapter implements DraggableItemAdapter<PlayingQueueAdapter.ViewHolder> {
private static final int HISTORY = 0;
private static final int CURRENT = 1;
private static final int UP_NEXT = 2;
private int current;
public PlayingQueueAdapter(AppCompatActivity activity, List<Song> dataSet, int current, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder);
this.current = current;
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SongAdapter.ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (holder.imageText != null) {
holder.imageText.setText(String.valueOf(position - current));
}
if (holder.getItemViewType() == HISTORY || holder.getItemViewType() == CURRENT) {
setAlpha(holder, 0.5f);
}
}
@Override
public int getItemViewType(int position) {
if (position < current) {
return HISTORY;
} else if (position > current) {
return UP_NEXT;
}
return CURRENT;
}
@Override
protected void loadAlbumCover(Song song, SongAdapter.ViewHolder holder) {
// don't want to load it in this adapter
}
public void swapDataSet(List<Song> dataSet, int position) {
this.dataSet = dataSet;
current = position;
notifyDataSetChanged();
}
public void setCurrent(int current) {
this.current = current;
notifyDataSetChanged();
}
protected void setAlpha(SongAdapter.ViewHolder holder, float alpha) {
if (holder.image != null) {
holder.image.setAlpha(alpha);
}
if (holder.title != null) {
holder.title.setAlpha(alpha);
}
if (holder.text != null) {
holder.text.setAlpha(alpha);
}
if (holder.imageText != null) {
holder.imageText.setAlpha(alpha);
}
if (holder.paletteColorContainer != null) {
holder.paletteColorContainer.setAlpha(alpha);
}
}
@Override
public boolean onCheckCanStartDrag(ViewHolder holder, int position, int x, int y) {
return ViewUtil.hitTest(holder.imageText, x, y);
}
@Override
public ItemDraggableRange onGetItemDraggableRange(ViewHolder holder, int position) {
return null;
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
MusicPlayerRemote.moveSong(fromPosition, toPosition);
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return true;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
public class ViewHolder extends SongAdapter.ViewHolder implements DraggableItemViewHolder {
@DraggableItemStateFlags
private int mDragStateFlags;
public ViewHolder(@NonNull View itemView) {
super(itemView);
if (imageText != null) {
imageText.setVisibility(View.VISIBLE);
}
if (image != null) {
image.setVisibility(View.GONE);
}
}
@Override
protected int getSongMenuRes() {
return R.menu.menu_item_playing_queue_song;
}
@Override
protected boolean onSongMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_remove_from_queue:
MusicPlayerRemote.removeFromQueue(getAdapterPosition());
return true;
}
return super.onSongMenuItemClick(item);
}
@Override
public void setDragStateFlags(@DraggableItemStateFlags int flags) {
mDragStateFlags = flags;
}
@Override
@DraggableItemStateFlags
public int getDragStateFlags() {
return mDragStateFlags;
}
}
}

View file

@ -0,0 +1,94 @@
package com.dkanada.gramophone.adapter.song;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import java.util.ArrayList;
import java.util.List;
public class PlaylistSongAdapter extends AbsOffsetSongAdapter {
public PlaylistSongAdapter(AppCompatActivity activity, @NonNull List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder, false);
setMultiSelectMenuRes(R.menu.menu_cannot_delete_single_songs_playlist_songs_selection);
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new PlaylistSongAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final SongAdapter.ViewHolder holder, int position) {
if (holder.getItemViewType() == OFFSET_ITEM) {
int textColor = ThemeStore.textColorSecondary(activity);
if (holder.title != null) {
holder.title.setText(MusicUtil.getPlaylistInfoString(activity, dataSet));
holder.title.setTextColor(textColor);
}
if (holder.text != null) {
holder.text.setVisibility(View.GONE);
}
if (holder.menu != null) {
holder.menu.setVisibility(View.GONE);
}
if (holder.image != null) {
final int padding = activity.getResources().getDimensionPixelSize(R.dimen.default_item_margin) / 2;
holder.image.setPadding(padding, padding, padding, padding);
holder.image.setColorFilter(textColor);
holder.image.setImageResource(R.drawable.ic_timer_white_24dp);
}
if (holder.dragView != null) {
holder.dragView.setVisibility(View.GONE);
}
if (holder.separator != null) {
holder.separator.setVisibility(View.VISIBLE);
}
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
super.onBindViewHolder(holder, position - 1);
}
}
public class ViewHolder extends AbsOffsetSongAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
protected int getSongMenuRes() {
return R.menu.menu_item_cannot_delete_single_songs_playlist_song;
}
@Override
protected boolean onSongMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.action_go_to_album) {
Pair[] albumPairs = new Pair[]{Pair.create(image, activity.getString(R.string.transition_album_art))};
NavigationUtil.goToAlbum(activity, new Album(dataSet.get(getAdapterPosition() - 1)), albumPairs);
return true;
}
return super.onSongMenuItemClick(item);
}
}
}

View file

@ -0,0 +1,82 @@
package com.dkanada.gramophone.adapter.song;
import android.graphics.Typeface;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Song;
import java.util.ArrayList;
import java.util.List;
public class ShuffleButtonSongAdapter extends AbsOffsetSongAdapter {
public ShuffleButtonSongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
super(activity, dataSet, itemLayoutRes, usePalette, cabHolder);
}
@Override
protected SongAdapter.ViewHolder createViewHolder(View view) {
return new ShuffleButtonSongAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final SongAdapter.ViewHolder holder, int position) {
if (holder.getItemViewType() == OFFSET_ITEM) {
int accentColor = ThemeStore.accentColor(activity);
if (holder.title != null) {
holder.title.setText(activity.getResources().getString(R.string.action_shuffle_all).toUpperCase());
holder.title.setTextColor(accentColor);
holder.title.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
}
if (holder.text != null) {
holder.text.setVisibility(View.GONE);
}
if (holder.menu != null) {
holder.menu.setVisibility(View.GONE);
}
if (holder.image != null) {
final int padding = activity.getResources().getDimensionPixelSize(R.dimen.default_item_margin) / 2;
holder.image.setPadding(padding, padding, padding, padding);
holder.image.setColorFilter(accentColor);
holder.image.setImageResource(R.drawable.ic_shuffle_white_24dp);
}
if (holder.separator != null) {
holder.separator.setVisibility(View.VISIBLE);
}
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
super.onBindViewHolder(holder, position - 1);
}
}
public class ViewHolder extends AbsOffsetSongAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
public void onClick(View v) {
if (getItemViewType() == OFFSET_ITEM) {
MusicPlayerRemote.openAndShuffleQueue(dataSet, true);
return;
}
super.onClick(v);
}
}
}

View file

@ -0,0 +1,276 @@
package com.dkanada.gramophone.adapter.song;
import android.graphics.drawable.Drawable;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.afollestad.materialcab.MaterialCab;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.base.AbsMultiSelectAdapter;
import com.dkanada.gramophone.adapter.base.MediaEntryViewHolder;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.helper.menu.SongMenuHelper;
import com.dkanada.gramophone.helper.menu.SongsMenuHelper;
import com.dkanada.gramophone.helper.sort.SortMethod;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import java.util.List;
public class SongAdapter extends AbsMultiSelectAdapter<SongAdapter.ViewHolder, Song> implements MaterialCab.Callback, FastScrollRecyclerView.SectionedAdapter {
protected final AppCompatActivity activity;
protected List<Song> dataSet;
protected int itemLayoutRes;
protected boolean usePalette;
protected boolean showSectionName;
public SongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder) {
this(activity, dataSet, itemLayoutRes, usePalette, cabHolder, true);
}
public SongAdapter(AppCompatActivity activity, List<Song> dataSet, @LayoutRes int itemLayoutRes, boolean usePalette, @Nullable CabHolder cabHolder, boolean showSectionName) {
super(activity, cabHolder, R.menu.menu_media_selection);
this.activity = activity;
this.dataSet = dataSet;
this.itemLayoutRes = itemLayoutRes;
this.usePalette = usePalette;
this.showSectionName = showSectionName;
setHasStableIds(true);
}
public void swapDataSet(List<Song> dataSet) {
this.dataSet = dataSet;
notifyDataSetChanged();
}
public void usePalette(boolean usePalette) {
this.usePalette = usePalette;
notifyDataSetChanged();
}
public List<Song> getDataSet() {
return dataSet;
}
@Override
public long getItemId(int position) {
return dataSet.get(position).hashCode();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false);
return createViewHolder(view);
}
protected ViewHolder createViewHolder(View view) {
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
final Song song = dataSet.get(position);
boolean isChecked = isChecked(song);
holder.itemView.setActivated(isChecked);
if (holder.getAdapterPosition() == getItemCount() - 1) {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.GONE);
}
} else {
if (holder.shortSeparator != null) {
holder.shortSeparator.setVisibility(View.VISIBLE);
}
}
if (holder.title != null) {
holder.title.setText(getSongTitle(song));
}
if (holder.text != null) {
holder.text.setText(getSongText(song));
}
loadAlbumCover(song, holder);
}
private void setColors(int color, ViewHolder holder) {
if (holder.paletteColorContainer != null) {
holder.paletteColorContainer.setBackgroundColor(color);
if (holder.title != null) {
holder.title.setTextColor(MaterialValueHelper.getPrimaryTextColor(activity, ColorUtil.isColorLight(color)));
}
if (holder.text != null) {
holder.text.setTextColor(MaterialValueHelper.getSecondaryTextColor(activity, ColorUtil.isColorLight(color)));
}
}
}
protected void loadAlbumCover(Song song, final ViewHolder holder) {
if (holder.image == null) return;
CustomGlideRequest.Builder
.from(Glide.with(activity), song.primary)
.generatePalette(activity).build()
.into(new CustomPaletteTarget(holder.image) {
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
setColors(getDefaultFooterColor(), holder);
}
@Override
public void onColorReady(int color) {
if (usePalette) {
setColors(color, holder);
} else {
setColors(getDefaultFooterColor(), holder);
}
}
});
}
protected String getSongTitle(Song song) {
return song.title;
}
protected String getSongText(Song song) {
return MusicUtil.getSongInfoString(song);
}
@Override
public int getItemCount() {
return dataSet.size();
}
@Override
protected Song getIdentifier(int position) {
return dataSet.get(position);
}
@Override
protected String getName(Song song) {
return song.title;
}
@Override
protected void onMultipleItemAction(@NonNull MenuItem menuItem, @NonNull List<Song> selection) {
SongsMenuHelper.handleMenuClick(activity, selection, menuItem.getItemId());
}
@NonNull
@Override
public String getSectionName(int position) {
if (!showSectionName) {
return "";
}
@Nullable String sectionName = null;
switch (PreferenceUtil.getInstance(activity).getSongSortMethod()) {
case SortMethod.NAME:
sectionName = dataSet.get(position).title;
break;
case SortMethod.ALBUM:
sectionName = dataSet.get(position).albumName;
break;
case SortMethod.ARTIST:
sectionName = dataSet.get(position).artistName;
break;
case SortMethod.YEAR:
return MusicUtil.getYearString(dataSet.get(position).year);
case SortMethod.RANDOM:
return activity.getResources().getString(R.string.random);
}
return MusicUtil.getSectionName(sectionName);
}
public class ViewHolder extends MediaEntryViewHolder {
protected int DEFAULT_MENU_RES = SongMenuHelper.MENU_RES;
public ViewHolder(@NonNull View itemView) {
super(itemView);
setImageTransitionName(activity.getString(R.string.transition_album_art));
if (menu == null) {
return;
}
menu.setOnClickListener(new SongMenuHelper.OnClickSongMenu(activity) {
@Override
public Song getSong() {
return ViewHolder.this.getSong();
}
@Override
public int getMenuRes() {
return getSongMenuRes();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
return onSongMenuItemClick(item) || super.onMenuItemClick(item);
}
});
}
protected Song getSong() {
return dataSet.get(getAdapterPosition());
}
protected int getSongMenuRes() {
return DEFAULT_MENU_RES;
}
protected boolean onSongMenuItemClick(MenuItem item) {
if (image != null && image.getVisibility() == View.VISIBLE) {
switch (item.getItemId()) {
case R.id.action_go_to_album:
Pair[] albumPairs = new Pair[]{Pair.create(image, activity.getResources().getString(R.string.transition_album_art))};
NavigationUtil.goToAlbum(activity, new Album(getSong()), albumPairs);
return true;
}
}
return false;
}
@Override
public void onClick(View v) {
if (isInQuickSelectMode()) {
toggleChecked(getAdapterPosition());
} else {
MusicPlayerRemote.openQueue(dataSet, getAdapterPosition(), true);
}
}
@Override
public boolean onLongClick(View view) {
return toggleChecked(getAdapterPosition());
}
}
}

View file

@ -0,0 +1,77 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.PlaylistUtil;
import com.dkanada.gramophone.util.QueryUtil;
import java.util.ArrayList;
import java.util.List;
public class AddToPlaylistDialog extends DialogFragment {
@NonNull
public static AddToPlaylistDialog create(Song song) {
List<Song> list = new ArrayList<>();
list.add(song);
return create(list);
}
@NonNull
public static AddToPlaylistDialog create(List<Song> songs) {
AddToPlaylistDialog dialog = new AddToPlaylistDialog();
Bundle args = new Bundle();
args.putParcelableArrayList("songs", new ArrayList<>(songs));
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
List<Playlist> playlists = new ArrayList<>();
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.action_add_to_playlist)
.items(getActivity().getResources().getString(R.string.action_new_playlist))
.itemsCallback((materialDialog, view, i, charSequence) -> {
final List<Song> songs = getArguments().getParcelableArrayList("songs");
if (songs == null) return;
if (i == 0) {
materialDialog.dismiss();
CreatePlaylistDialog.create(songs).show(getActivity().getSupportFragmentManager(), "ADD_TO_PLAYLIST");
} else {
materialDialog.dismiss();
PlaylistUtil.addItems(songs, playlists.get(i - 1).id);
}
})
.build();
QueryUtil.getPlaylists(new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
playlists.addAll((List<Playlist>) media);
CharSequence[] names = new CharSequence[playlists.size() + 1];
names[0] = getActivity().getResources().getString(R.string.action_new_playlist);
for (int i = 0; i < playlists.size(); i++) {
names[i + 1] = playlists.get(i).name;
}
dialog.setItems(names);
}
});
return dialog;
}
}

View file

@ -0,0 +1,61 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import android.text.InputType;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.PlaylistUtil;
import java.util.ArrayList;
import java.util.List;
public class CreatePlaylistDialog extends DialogFragment {
private static final String SONGS = "songs";
@NonNull
public static CreatePlaylistDialog create() {
return create((Song) null);
}
@NonNull
public static CreatePlaylistDialog create(@Nullable Song song) {
List<Song> list = new ArrayList<>();
if (song != null) list.add(song);
return create(list);
}
@NonNull
public static CreatePlaylistDialog create(List<Song> songs) {
CreatePlaylistDialog dialog = new CreatePlaylistDialog();
Bundle args = new Bundle();
args.putParcelableArrayList(SONGS, new ArrayList<>(songs));
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new MaterialDialog.Builder(getActivity())
.title(R.string.action_new_playlist)
.positiveText(R.string.create_action)
.negativeText(android.R.string.cancel)
.inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME | InputType.TYPE_TEXT_FLAG_CAP_WORDS)
.input(R.string.playlist_name_empty, 0, false, (materialDialog, charSequence) -> {
final String name = charSequence.toString().trim();
if (getActivity() == null || getArguments() == null || name.isEmpty()) return;
List<Song> songs = getArguments().getParcelableArrayList(SONGS);
if (songs != null) {
PlaylistUtil.createPlaylist(name, songs);
}
})
.build();
}
}

View file

@ -0,0 +1,61 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.text.Html;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.util.PlaylistUtil;
import java.util.ArrayList;
import java.util.List;
public class DeletePlaylistDialog extends DialogFragment {
@NonNull
public static DeletePlaylistDialog create(Playlist playlist) {
List<Playlist> list = new ArrayList<>();
list.add(playlist);
return create(list);
}
@NonNull
public static DeletePlaylistDialog create(List<Playlist> playlists) {
DeletePlaylistDialog dialog = new DeletePlaylistDialog();
Bundle args = new Bundle();
args.putParcelableArrayList("playlists", new ArrayList<>(playlists));
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final List<Playlist> playlists = getArguments().getParcelableArrayList("playlists");
int title;
CharSequence content;
// noinspection ConstantConditions
if (playlists.size() > 1) {
title = R.string.delete_playlists_title;
content = Html.fromHtml(getString(R.string.delete_x_playlists, playlists.size()));
} else {
title = R.string.delete_playlist_title;
content = Html.fromHtml(getString(R.string.delete_playlist_x, playlists.get(0).name));
}
return new MaterialDialog.Builder(getActivity())
.title(title)
.content(content)
.positiveText(R.string.delete_action)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
if (getActivity() == null) return;
PlaylistUtil.deletePlaylist(playlists);
})
.build();
}
}

View file

@ -0,0 +1,66 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.text.Html;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.PlaylistSong;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.PlaylistUtil;
import java.util.ArrayList;
import java.util.List;
public class RemoveFromPlaylistDialog extends DialogFragment {
@NonNull
public static RemoveFromPlaylistDialog create(Song song) {
List<Song> list = new ArrayList<>();
list.add(song);
return create(list);
}
@NonNull
public static RemoveFromPlaylistDialog create(List<Song> songs) {
RemoveFromPlaylistDialog dialog = new RemoveFromPlaylistDialog();
Bundle args = new Bundle();
args.putParcelableArrayList("songs", new ArrayList<>(songs));
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final List<PlaylistSong> songs = getArguments().getParcelableArrayList("songs");
int title;
CharSequence content;
if (songs.size() > 1) {
title = R.string.remove_songs_from_playlist_title;
content = Html.fromHtml(getString(R.string.remove_x_songs_from_playlist, songs.size()));
} else {
title = R.string.remove_song_from_playlist_title;
content = Html.fromHtml(getString(R.string.remove_song_x_from_playlist, songs.get(0).title));
}
return new MaterialDialog.Builder(getActivity())
.title(title)
.content(content)
.positiveText(R.string.remove_action)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
if (getActivity() == null) return;
PlaylistSong song = songs.get(0);
PlaylistUtil.deleteItems(songs, song.playlistId);
})
.build();
}
}

View file

@ -0,0 +1,49 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.text.InputType;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.util.PlaylistUtil;
public class RenamePlaylistDialog extends DialogFragment {
private static final String PLAYLIST_ID = "playlist_id";
@NonNull
public static RenamePlaylistDialog create(Playlist playlist) {
RenamePlaylistDialog dialog = new RenamePlaylistDialog();
Bundle args = new Bundle();
args.putString(PLAYLIST_ID, playlist.id);
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String playlistId = getArguments().getString(PLAYLIST_ID);
return new MaterialDialog.Builder(getActivity())
.title(R.string.rename_playlist_title)
.positiveText(R.string.rename_action)
.negativeText(android.R.string.cancel)
.inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME | InputType.TYPE_TEXT_FLAG_CAP_WORDS)
.input(getString(R.string.playlist_name_empty), "", false,
(materialDialog, charSequence) -> {
final String name = charSequence.toString().trim();
if (!name.isEmpty()) {
String id = getArguments().getString(PLAYLIST_ID);
PlaylistUtil.renamePlaylist(id, name);
}
})
.build();
}
}

View file

@ -0,0 +1,196 @@
package com.dkanada.gramophone.dialogs;
import android.app.AlarmManager;
import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.internal.ThemeSingleton;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.service.MusicService;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.triggertrap.seekarc.SeekArc;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SleepTimerDialog extends DialogFragment {
@BindView(R.id.seek_arc)
SeekArc seekArc;
@BindView(R.id.timer_display)
TextView timerDisplay;
@BindView(R.id.should_finish_last_song)
CheckBox shouldFinishLastSong;
private int seekArcProgress;
private MaterialDialog materialDialog;
private TimerUpdater timerUpdater;
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
timerUpdater.cancel();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
timerUpdater = new TimerUpdater();
materialDialog = new MaterialDialog.Builder(getActivity())
.title(getActivity().getResources().getString(R.string.action_sleep_timer))
.positiveText(R.string.action_set)
.onPositive((dialog, which) -> {
if (getActivity() == null) {
return;
}
PreferenceUtil.getInstance(getActivity()).setSleepTimerFinishMusic(shouldFinishLastSong.isChecked());
final int minutes = seekArcProgress;
PendingIntent pi = makeTimerPendingIntent(PendingIntent.FLAG_CANCEL_CURRENT);
final long nextSleepTimerElapsedTime = SystemClock.elapsedRealtime() + minutes * 60 * 1000;
PreferenceUtil.getInstance(getActivity()).setNextSleepTimerElapsedRealtime(nextSleepTimerElapsedTime);
AlarmManager am = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextSleepTimerElapsedTime, pi);
Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.sleep_timer_set, minutes), Toast.LENGTH_SHORT).show();
})
.onNeutral((dialog, which) -> {
if (getActivity() == null) {
return;
}
final PendingIntent previous = makeTimerPendingIntent(PendingIntent.FLAG_NO_CREATE);
if (previous != null) {
AlarmManager am = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
am.cancel(previous);
previous.cancel();
Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.sleep_timer_canceled), Toast.LENGTH_SHORT).show();
}
MusicService musicService = MusicPlayerRemote.musicService;
if (musicService != null && musicService.pendingQuit) {
musicService.pendingQuit = false;
Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.sleep_timer_canceled), Toast.LENGTH_SHORT).show();
}
})
.showListener(dialog -> {
if (makeTimerPendingIntent(PendingIntent.FLAG_NO_CREATE) != null) {
timerUpdater.start();
}
})
.customView(R.layout.dialog_sleep_timer, false)
.build();
if (getActivity() == null || materialDialog.getCustomView() == null) {
return materialDialog;
}
ButterKnife.bind(this, materialDialog.getCustomView());
boolean finishMusic = PreferenceUtil.getInstance(getActivity()).getSleepTimerFinishMusic();
shouldFinishLastSong.setChecked(finishMusic);
seekArc.setProgressColor(ThemeSingleton.get().positiveColor.getDefaultColor());
seekArc.setThumbColor(ThemeSingleton.get().positiveColor.getDefaultColor());
seekArc.post(() -> {
int width = seekArc.getWidth();
int height = seekArc.getHeight();
int small = Math.min(width, height);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(seekArc.getLayoutParams());
layoutParams.height = small;
seekArc.setLayoutParams(layoutParams);
});
seekArcProgress = PreferenceUtil.getInstance(getActivity()).getLastSleepTimerValue();
updateTimeDisplayTime();
seekArc.setProgress(seekArcProgress);
seekArc.setOnSeekArcChangeListener(new SeekArc.OnSeekArcChangeListener() {
@Override
public void onProgressChanged(@NonNull SeekArc seekArc, int i, boolean b) {
if (i < 1) {
seekArc.setProgress(1);
return;
}
seekArcProgress = i;
updateTimeDisplayTime();
}
@Override
public void onStartTrackingTouch(SeekArc seekArc) {
}
@Override
public void onStopTrackingTouch(SeekArc seekArc) {
PreferenceUtil.getInstance(getActivity()).setLastSleepTimerValue(seekArcProgress);
}
});
return materialDialog;
}
private void updateTimeDisplayTime() {
timerDisplay.setText(seekArcProgress + " min");
}
private PendingIntent makeTimerPendingIntent(int flag) {
return PendingIntent.getService(getActivity(), 0, makeTimerIntent(), flag);
}
private Intent makeTimerIntent() {
Intent intent = new Intent(getActivity(), MusicService.class);
if (shouldFinishLastSong.isChecked()) {
return intent.setAction(MusicService.ACTION_PENDING_QUIT);
}
return intent.setAction(MusicService.ACTION_QUIT);
}
private void updateCancelButton() {
MusicService musicService = MusicPlayerRemote.musicService;
if (musicService != null && musicService.pendingQuit) {
materialDialog.setActionButton(DialogAction.NEUTRAL, materialDialog.getContext().getString(R.string.cancel_current_timer));
} else {
materialDialog.setActionButton(DialogAction.NEUTRAL, null);
}
}
private class TimerUpdater extends CountDownTimer {
public TimerUpdater() {
super(PreferenceUtil.getInstance(getActivity()).getNextSleepTimerElapsedRealTime() - SystemClock.elapsedRealtime(), 1000);
}
@Override
public void onTick(long millisUntilFinished) {
materialDialog.setActionButton(DialogAction.NEUTRAL, materialDialog.getContext().getString(R.string.cancel_current_timer) + " (" + MusicUtil.getReadableDurationString(millisUntilFinished) + ")");
}
@Override
public void onFinish() {
updateCancelButton();
}
}
}

View file

@ -0,0 +1,75 @@
package com.dkanada.gramophone.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.text.Html;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
public class SongDetailDialog extends DialogFragment {
@NonNull
public static SongDetailDialog create(Song song) {
SongDetailDialog dialog = new SongDetailDialog();
Bundle args = new Bundle();
args.putParcelable("song", song);
dialog.setArguments(args);
return dialog;
}
private static Spanned makeTextWithTitle(@NonNull Context context, int titleResId, String text) {
return Html.fromHtml("<b>" + context.getResources().getString(titleResId) + ": " + "</b>" + text);
}
private static String getFileSizeString(long sizeInBytes) {
long fileSizeInKB = sizeInBytes / 1024;
long fileSizeInMB = fileSizeInKB / 1024;
return fileSizeInMB + " MB";
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity context = getActivity();
final Song song = getArguments().getParcelable("song");
MaterialDialog dialog = new MaterialDialog.Builder(context)
.customView(R.layout.dialog_file_details, true)
.title(context.getResources().getString(R.string.label_details))
.positiveText(android.R.string.ok)
.build();
View dialogView = dialog.getCustomView();
final TextView fileName = dialogView.findViewById(R.id.file_name);
final TextView filePath = dialogView.findViewById(R.id.file_path);
final TextView fileSize = dialogView.findViewById(R.id.file_size);
final TextView fileFormat = dialogView.findViewById(R.id.file_format);
final TextView trackLength = dialogView.findViewById(R.id.track_length);
final TextView bitRate = dialogView.findViewById(R.id.bitrate);
final TextView samplingRate = dialogView.findViewById(R.id.sampling_rate);
fileName.setText(makeTextWithTitle(context, R.string.label_file_name, "-"));
filePath.setText(makeTextWithTitle(context, R.string.label_file_path, "-"));
fileSize.setText(makeTextWithTitle(context, R.string.label_file_size, "-"));
fileFormat.setText(makeTextWithTitle(context, R.string.label_file_format, "-"));
trackLength.setText(makeTextWithTitle(context, R.string.label_track_length, "-"));
bitRate.setText(makeTextWithTitle(context, R.string.label_bit_rate, "-"));
samplingRate.setText(makeTextWithTitle(context, R.string.label_sampling_rate, "-"));
if (song != null) {
fileName.setText(makeTextWithTitle(context, R.string.label_file_name, song.title));
trackLength.setText(makeTextWithTitle(context, R.string.label_track_length, MusicUtil.getReadableDurationString(song.duration)));
}
return dialog;
}
}

View file

@ -0,0 +1,53 @@
package com.dkanada.gramophone.dialogs;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
public class SongShareDialog extends DialogFragment {
@NonNull
public static SongShareDialog create(final Song song) {
final SongShareDialog dialog = new SongShareDialog();
final Bundle args = new Bundle();
args.putParcelable("song", song);
dialog.setArguments(args);
return dialog;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Song song = getArguments().getParcelable("song");
final String currentlyListening = getString(R.string.currently_listening_to_x_by_x, song.title, song.artistName);
return new MaterialDialog.Builder(getActivity())
.title(R.string.what_do_you_want_to_share)
.items(getString(R.string.the_audio_file), "\u201C" + currentlyListening + "\u201D")
.itemsCallback((materialDialog, view, i, charSequence) -> {
switch (i) {
case 0:
startActivity(Intent.createChooser(MusicUtil.createShareSongFileIntent(song, getContext()), null));
break;
case 1:
getActivity().startActivity(
Intent.createChooser(
new Intent()
.setAction(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_TEXT, currentlyListening)
.setType("text/plain"),
null
)
);
break;
}
})
.build();
}
}

View file

@ -0,0 +1,144 @@
package com.dkanada.gramophone.glide;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RSRuntimeException;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.dkanada.gramophone.BuildConfig;
import com.dkanada.gramophone.helper.StackBlur;
import com.dkanada.gramophone.util.ImageUtil;
public class BlurTransformation extends BitmapTransformation {
public static final float DEFAULT_BLUR_RADIUS = 5f;
private Context context;
private float blurRadius;
private int sampling;
private void init(Builder builder) {
this.context = builder.context;
this.blurRadius = builder.blurRadius;
this.sampling = builder.sampling;
}
private BlurTransformation(Builder builder) {
super(builder.context);
init(builder);
}
private BlurTransformation(Builder builder, BitmapPool bitmapPool) {
super(bitmapPool);
init(builder);
}
public static class Builder {
private Context context;
private BitmapPool bitmapPool;
private float blurRadius = DEFAULT_BLUR_RADIUS;
private int sampling;
public Builder(@NonNull Context context) {
this.context = context;
}
/**
* @param blurRadius The radius to use. Must be between 0 and 25. Default is 5.
* @return the same Builder
*/
public Builder blurRadius(@FloatRange(from = 0.0f, to = 25.0f) float blurRadius) {
this.blurRadius = blurRadius;
return this;
}
/**
* @param sampling The inSampleSize to use. Must be a power of 2, or 1 for no down sampling or 0 for auto detect sampling. Default is 0.
* @return the same Builder
*/
public Builder sampling(int sampling) {
this.sampling = sampling;
return this;
}
/**
* @param bitmapPool The BitmapPool to use.
* @return the same Builder
*/
public Builder bitmapPool(BitmapPool bitmapPool) {
this.bitmapPool = bitmapPool;
return this;
}
public BlurTransformation build() {
if (bitmapPool != null) {
return new BlurTransformation(this, bitmapPool);
}
return new BlurTransformation(this);
}
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
int sampling;
if (this.sampling == 0) {
sampling = ImageUtil.calculateInSampleSize(toTransform.getWidth(), toTransform.getHeight(), 100);
} else {
sampling = this.sampling;
}
int width = toTransform.getWidth();
int height = toTransform.getHeight();
int scaledWidth = width / sampling;
int scaledHeight = height / sampling;
Bitmap out = pool.get(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
if (out == null) {
out = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(out);
canvas.scale(1 / (float) sampling, 1 / (float) sampling);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(toTransform, 0, 0, paint);
if (Build.VERSION.SDK_INT >= 17) {
try {
final RenderScript rs = RenderScript.create(context.getApplicationContext());
final Allocation input = Allocation.createFromBitmap(rs, out, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
final Allocation output = Allocation.createTyped(rs, input.getType());
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(blurRadius);
script.setInput(input);
script.forEach(output);
output.copyTo(out);
rs.destroy();
return out;
} catch (RSRuntimeException e) {
// on some devices RenderScript.create() throws: android.support.v8.renderscript.RSRuntimeException: Error loading libRSSupport library
if (BuildConfig.DEBUG) e.printStackTrace();
}
}
return StackBlur.blur(out, blurRadius);
}
@Override
public String getId() {
return "BlurTransformation(radius=" + blurRadius + ", sampling=" + sampling + ")";
}
}

View file

@ -0,0 +1,118 @@
package com.dkanada.gramophone.glide;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import com.bumptech.glide.BitmapRequestBuilder;
import com.bumptech.glide.DrawableRequestBuilder;
import com.bumptech.glide.DrawableTypeRequest;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.signature.MediaStoreSignature;
import com.dkanada.gramophone.App;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.palette.BitmapPaletteTranscoder;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import org.jellyfin.apiclient.model.dto.ImageOptions;
import org.jellyfin.apiclient.model.entities.ImageType;
public class CustomGlideRequest {
public static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.ALL;
public static final int DEFAULT_IMAGE = R.drawable.default_album_art;
public static final int DEFAULT_ANIMATION = android.R.anim.fade_in;
public static class Builder {
final RequestManager requestManager;
final String item;
public static Builder from(@NonNull RequestManager requestManager, String item) {
return new Builder(requestManager, item);
}
private Builder(@NonNull RequestManager requestManager, String item) {
this.requestManager = requestManager;
this.item = item;
}
public PaletteBuilder generatePalette(Context context) {
return new PaletteBuilder(this, context);
}
public BitmapBuilder asBitmap() {
return new BitmapBuilder(this);
}
public DrawableRequestBuilder<GlideDrawable> build() {
// noinspection unchecked
return createBaseRequest(requestManager, item)
.diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY)
.placeholder(DEFAULT_IMAGE)
.animate(DEFAULT_ANIMATION)
.signature(createSignature(item));
}
}
public static class BitmapBuilder {
private final Builder builder;
public BitmapBuilder(Builder builder) {
this.builder = builder;
}
public BitmapRequestBuilder<?, Bitmap> build() {
// noinspection unchecked
return createBaseRequest(builder.requestManager, builder.item)
.asBitmap()
.diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY)
.placeholder(DEFAULT_IMAGE)
.animate(DEFAULT_ANIMATION)
.signature(createSignature(builder.item));
}
}
public static class PaletteBuilder {
final Context context;
private final Builder builder;
public PaletteBuilder(Builder builder, Context context) {
this.builder = builder;
this.context = context;
}
public BitmapRequestBuilder<?, BitmapPaletteWrapper> build() {
// noinspection unchecked
return createBaseRequest(builder.requestManager, builder.item)
.asBitmap()
.transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class)
.diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY)
.encoder(new BitmapEncoder(Bitmap.CompressFormat.PNG, 100))
.placeholder(DEFAULT_IMAGE)
.animate(DEFAULT_ANIMATION)
.signature(createSignature(builder.item));
}
}
public static DrawableTypeRequest createBaseRequest(RequestManager requestManager, String item) {
if (item == null) {
return requestManager.load(R.drawable.default_album_art);
}
ImageOptions options = new ImageOptions();
options.setImageType(ImageType.Primary);
options.setMaxHeight(800);
String url = App.getApiClient().GetImageUrl(item, options);
return requestManager.load(url);
}
public static Key createSignature(String item) {
return new MediaStoreSignature("image/jpeg", item != null ? item.hashCode() : 0, 0);
}
}

View file

@ -0,0 +1,39 @@
package com.dkanada.gramophone.glide;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.kabouzeid.appthemehelper.util.ATHUtil;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.palette.BitmapPaletteTarget;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import com.dkanada.gramophone.util.ThemeUtil;
public abstract class CustomPaletteTarget extends BitmapPaletteTarget {
public CustomPaletteTarget(ImageView view) {
super(view);
}
@Override
public void onLoadFailed(Exception e, Drawable errorDrawable) {
super.onLoadFailed(e, errorDrawable);
onColorReady(getDefaultFooterColor());
}
@Override
public void onResourceReady(BitmapPaletteWrapper resource, GlideAnimation<? super BitmapPaletteWrapper> glideAnimation) {
super.onResourceReady(resource, glideAnimation);
onColorReady(ThemeUtil.getColor(resource.getPalette(), getDefaultFooterColor()));
}
protected int getDefaultFooterColor() {
return ATHUtil.resolveColor(getView().getContext(), R.attr.defaultFooterColor);
}
protected int getAlbumArtistFooterColor() {
return ATHUtil.resolveColor(getView().getContext(), R.attr.cardBackgroundColor);
}
public abstract void onColorReady(int color);
}

View file

@ -0,0 +1,33 @@
package com.dkanada.gramophone.glide.palette;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.Util;
public class BitmapPaletteResource implements Resource<BitmapPaletteWrapper> {
private final BitmapPaletteWrapper bitmapPaletteWrapper;
private final BitmapPool bitmapPool;
public BitmapPaletteResource(BitmapPaletteWrapper bitmapPaletteWrapper, BitmapPool bitmapPool) {
this.bitmapPaletteWrapper = bitmapPaletteWrapper;
this.bitmapPool = bitmapPool;
}
@Override
public BitmapPaletteWrapper get() {
return bitmapPaletteWrapper;
}
@Override
public int getSize() {
return Util.getBitmapByteSize(bitmapPaletteWrapper.getBitmap());
}
@Override
public void recycle() {
if (!bitmapPool.put(bitmapPaletteWrapper.getBitmap())) {
bitmapPaletteWrapper.getBitmap().recycle();
}
}
}

View file

@ -0,0 +1,16 @@
package com.dkanada.gramophone.glide.palette;
import android.widget.ImageView;
import com.bumptech.glide.request.target.ImageViewTarget;
public class BitmapPaletteTarget extends ImageViewTarget<BitmapPaletteWrapper> {
public BitmapPaletteTarget(ImageView view) {
super(view);
}
@Override
protected void setResource(BitmapPaletteWrapper bitmapPaletteWrapper) {
view.setImageBitmap(bitmapPaletteWrapper.getBitmap());
}
}

View file

@ -0,0 +1,34 @@
package com.dkanada.gramophone.glide.palette;
import android.content.Context;
import android.graphics.Bitmap;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.dkanada.gramophone.util.ThemeUtil;
public class BitmapPaletteTranscoder implements ResourceTranscoder<Bitmap, BitmapPaletteWrapper> {
private final BitmapPool bitmapPool;
public BitmapPaletteTranscoder(Context context) {
this(Glide.get(context).getBitmapPool());
}
public BitmapPaletteTranscoder(BitmapPool bitmapPool) {
this.bitmapPool = bitmapPool;
}
@Override
public Resource<BitmapPaletteWrapper> transcode(Resource<Bitmap> bitmapResource) {
Bitmap bitmap = bitmapResource.get();
BitmapPaletteWrapper bitmapPaletteWrapper = new BitmapPaletteWrapper(bitmap, ThemeUtil.generatePalette(bitmap));
return new BitmapPaletteResource(bitmapPaletteWrapper, bitmapPool);
}
@Override
public String getId() {
return "BitmapPaletteTranscoder.com.kabouzeid.gramophone.glide.palette";
}
}

View file

@ -0,0 +1,22 @@
package com.dkanada.gramophone.glide.palette;
import android.graphics.Bitmap;
import androidx.palette.graphics.Palette;
public class BitmapPaletteWrapper {
private final Bitmap mBitmap;
private final Palette mPalette;
public BitmapPaletteWrapper(Bitmap bitmap, Palette palette) {
mBitmap = bitmap;
mPalette = palette;
}
public Bitmap getBitmap() {
return mBitmap;
}
public Palette getPalette() {
return mPalette;
}
}

View file

@ -0,0 +1,31 @@
package com.dkanada.gramophone.helper;
import android.content.Context;
import android.view.ViewGroup;
import com.dkanada.gramophone.R;
public class HorizontalAdapterHelper {
public static final int LAYOUT_RES = R.layout.item_grid_card_horizontal;
public static final int TYPE_FIRST = 1;
public static final int TYPE_MIDDLE = 2;
public static final int TYPE_LAST = 3;
public static void applyMarginToLayoutParams(Context context, ViewGroup.MarginLayoutParams layoutParams, int viewType) {
int listMargin = context.getResources().getDimensionPixelSize(R.dimen.default_item_margin);
if (viewType == TYPE_FIRST) {
layoutParams.leftMargin = listMargin;
} else if (viewType == TYPE_LAST) {
layoutParams.rightMargin = listMargin;
}
}
public static int getItemViewtype(int position, int itemCount) {
if (position == 0) {
return TYPE_FIRST;
} else if (position == itemCount - 1) {
return TYPE_LAST;
} else return TYPE_MIDDLE;
}
}

View file

@ -0,0 +1,366 @@
package com.dkanada.gramophone.helper;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.widget.Toast;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.service.MusicService;
import com.dkanada.gramophone.util.PreferenceUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.WeakHashMap;
public class MusicPlayerRemote {
public static MusicService musicService;
private static final WeakHashMap<Context, ServiceBinder> mConnectionMap = new WeakHashMap<>();
public static ServiceToken bindToService(@NonNull final Context context,
final ServiceConnection callback) {
Activity realActivity = ((Activity) context).getParent();
if (realActivity == null) {
realActivity = (Activity) context;
}
final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
contextWrapper.startService(new Intent(contextWrapper, MusicService.class));
final ServiceBinder binder = new ServiceBinder(callback);
if (contextWrapper.bindService(new Intent().setClass(contextWrapper, MusicService.class), binder, Context.BIND_AUTO_CREATE)) {
mConnectionMap.put(contextWrapper, binder);
return new ServiceToken(contextWrapper);
}
return null;
}
public static void unbindFromService(@Nullable final ServiceToken token) {
if (token == null) {
return;
}
final ContextWrapper mContextWrapper = token.mWrappedContext;
final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
if (mBinder == null) {
return;
}
mContextWrapper.unbindService(mBinder);
if (mConnectionMap.isEmpty()) {
musicService = null;
}
}
public static final class ServiceBinder implements ServiceConnection {
private final ServiceConnection mCallback;
public ServiceBinder(final ServiceConnection callback) {
mCallback = callback;
}
@Override
public void onServiceConnected(final ComponentName className, final IBinder service) {
MusicService.MusicBinder binder = (MusicService.MusicBinder) service;
musicService = binder.getService();
if (mCallback != null) {
mCallback.onServiceConnected(className, service);
}
}
@Override
public void onServiceDisconnected(final ComponentName className) {
if (mCallback != null) {
mCallback.onServiceDisconnected(className);
}
musicService = null;
}
}
public static final class ServiceToken {
public ContextWrapper mWrappedContext;
public ServiceToken(final ContextWrapper context) {
mWrappedContext = context;
}
}
public static void playSongAt(final int position) {
if (musicService != null) {
musicService.playSongAt(position);
}
}
public static void setPosition(final int position) {
if (musicService != null) {
musicService.setPosition(position);
}
}
public static void pauseSong() {
if (musicService != null) {
musicService.pause();
}
}
public static void playNextSong() {
if (musicService != null) {
musicService.playNextSong(true);
}
}
public static void playPreviousSong() {
if (musicService != null) {
musicService.playPreviousSong(true);
}
}
public static void back() {
if (musicService != null) {
musicService.back(true);
}
}
public static boolean isPlaying() {
return musicService != null && musicService.isPlaying();
}
public static void resumePlaying() {
if (musicService != null) {
musicService.play();
}
}
public static void openQueue(final List<Song> queue, final int startPosition, final boolean startPlaying) {
if (!tryToHandleOpenPlayingQueue(queue, startPosition, startPlaying) && musicService != null) {
musicService.openQueue(queue, startPosition, startPlaying);
if (!PreferenceUtil.getInstance(musicService).getRememberShuffle()){
setShuffleMode(MusicService.SHUFFLE_MODE_NONE);
}
}
}
public static void openAndShuffleQueue(final List<Song> queue, boolean startPlaying) {
int startPosition = 0;
if (!queue.isEmpty()) {
startPosition = new Random().nextInt(queue.size());
}
if (!tryToHandleOpenPlayingQueue(queue, startPosition, startPlaying) && musicService != null) {
openQueue(queue, startPosition, startPlaying);
setShuffleMode(MusicService.SHUFFLE_MODE_SHUFFLE);
}
}
private static boolean tryToHandleOpenPlayingQueue(final List<Song> queue, final int startPosition, final boolean startPlaying) {
if (getPlayingQueue() == queue) {
if (startPlaying) {
playSongAt(startPosition);
} else {
setPosition(startPosition);
}
return true;
}
return false;
}
public static Song getCurrentSong() {
if (musicService != null) {
return musicService.getCurrentSong();
}
return Song.EMPTY_SONG;
}
public static int getPosition() {
if (musicService != null) {
return musicService.getPosition();
}
return -1;
}
public static List<Song> getPlayingQueue() {
if (musicService != null) {
return musicService.getPlayingQueue();
}
return new ArrayList<>();
}
public static int getSongProgressMillis() {
if (musicService != null) {
return musicService.getSongProgressMillis();
}
return -1;
}
public static int getSongDurationMillis() {
if (musicService != null) {
return musicService.getSongDurationMillis();
}
return -1;
}
public static long getQueueDurationMillis(int position) {
if (musicService != null) {
return musicService.getQueueDurationMillis(position);
}
return -1;
}
public static int seekTo(int millis) {
if (musicService != null) {
return musicService.seek(millis);
}
return -1;
}
public static int getRepeatMode() {
if (musicService != null) {
return musicService.getRepeatMode();
}
return MusicService.REPEAT_MODE_NONE;
}
public static int getShuffleMode() {
if (musicService != null) {
return musicService.getShuffleMode();
}
return MusicService.SHUFFLE_MODE_NONE;
}
public static boolean cycleRepeatMode() {
if (musicService != null) {
musicService.cycleRepeatMode();
return true;
}
return false;
}
public static boolean toggleShuffleMode() {
if (musicService != null) {
musicService.toggleShuffle();
return true;
}
return false;
}
public static boolean setShuffleMode(final int shuffleMode) {
if (musicService != null) {
musicService.setShuffleMode(shuffleMode);
return true;
}
return false;
}
public static boolean playNext(Song song) {
if (musicService != null) {
if (getPlayingQueue().size() > 0) {
musicService.addSong(getPosition() + 1, song);
} else {
List<Song> queue = new ArrayList<>();
queue.add(song);
openQueue(queue, 0, false);
}
Toast.makeText(musicService, musicService.getResources().getString(R.string.added_title_to_queue), Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
public static boolean playNext(@NonNull List<Song> songs) {
if (musicService != null) {
if (getPlayingQueue().size() > 0) {
musicService.addSongs(getPosition() + 1, songs);
} else {
openQueue(songs, 0, false);
}
final String toast = songs.size() == 1 ? musicService.getResources().getString(R.string.added_title_to_queue) : musicService.getResources().getString(R.string.added_x_titles_to_queue, songs.size());
Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
public static boolean enqueue(Song song) {
if (musicService != null) {
if (getPlayingQueue().size() > 0) {
musicService.addSong(song);
} else {
List<Song> queue = new ArrayList<>();
queue.add(song);
openQueue(queue, 0, false);
}
Toast.makeText(musicService, musicService.getResources().getString(R.string.added_title_to_queue), Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
public static boolean enqueue(@NonNull List<Song> songs) {
if (musicService != null) {
if (getPlayingQueue().size() > 0) {
musicService.addSongs(songs);
} else {
openQueue(songs, 0, false);
}
final String toast = songs.size() == 1 ? musicService.getResources().getString(R.string.added_title_to_queue) : musicService.getResources().getString(R.string.added_x_titles_to_queue, songs.size());
Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
public static boolean removeFromQueue(@NonNull Song song) {
if (musicService != null) {
musicService.removeSong(song);
return true;
}
return false;
}
public static boolean removeFromQueue(int position) {
if (musicService != null && position >= 0 && position < getPlayingQueue().size()) {
musicService.removeSong(position);
return true;
}
return false;
}
public static boolean moveSong(int from, int to) {
if (musicService != null && from >= 0 && to >= 0 && from < getPlayingQueue().size() && to < getPlayingQueue().size()) {
musicService.moveSong(from, to);
return true;
}
return false;
}
public static boolean clearQueue() {
if (musicService != null) {
musicService.clearQueue();
return true;
}
return false;
}
}

View file

@ -0,0 +1,70 @@
package com.dkanada.gramophone.helper;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
public class MusicProgressViewUpdateHelper extends Handler {
private static final int CMD_REFRESH_PROGRESS_VIEWS = 1;
private static final int MIN_INTERVAL = 20;
private static final int UPDATE_INTERVAL_PLAYING = 1000;
private static final int UPDATE_INTERVAL_PAUSED = 500;
private Callback callback;
private int intervalPlaying;
private int intervalPaused;
public void start() {
queueNextRefresh(1);
}
public void stop() {
removeMessages(CMD_REFRESH_PROGRESS_VIEWS);
}
public MusicProgressViewUpdateHelper(Callback callback) {
this.callback = callback;
this.intervalPlaying = UPDATE_INTERVAL_PLAYING;
this.intervalPaused = UPDATE_INTERVAL_PAUSED;
}
public MusicProgressViewUpdateHelper(Callback callback, int intervalPlaying, int intervalPaused) {
this.callback = callback;
this.intervalPlaying = intervalPlaying;
this.intervalPaused = intervalPaused;
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == CMD_REFRESH_PROGRESS_VIEWS) {
queueNextRefresh(refreshProgressViews());
}
}
private int refreshProgressViews() {
final int progressMillis = MusicPlayerRemote.getSongProgressMillis();
final int totalMillis = MusicPlayerRemote.getSongDurationMillis();
callback.onUpdateProgressViews(progressMillis, totalMillis);
if (!MusicPlayerRemote.isPlaying()) {
return intervalPaused;
}
final int remainingMillis = intervalPlaying - progressMillis % intervalPlaying;
return Math.max(MIN_INTERVAL, remainingMillis);
}
private void queueNextRefresh(final long delay) {
final Message message = obtainMessage(CMD_REFRESH_PROGRESS_VIEWS);
removeMessages(CMD_REFRESH_PROGRESS_VIEWS);
sendMessageDelayed(message, delay);
}
public interface Callback {
void onUpdateProgressViews(int progress, int total);
}
}

View file

@ -0,0 +1,14 @@
package com.dkanada.gramophone.helper;
import android.view.View;
public class PlayPauseButtonOnClickHandler implements View.OnClickListener {
@Override
public void onClick(View v) {
if (MusicPlayerRemote.isPlaying()) {
MusicPlayerRemote.pauseSong();
} else {
MusicPlayerRemote.resumePlaying();
}
}
}

View file

@ -0,0 +1,22 @@
package com.dkanada.gramophone.helper;
import androidx.annotation.NonNull;
import com.dkanada.gramophone.model.Song;
import java.util.Collections;
import java.util.List;
public class ShuffleHelper {
public static void makeShuffleList(@NonNull List<Song> listToShuffle, final int current) {
if (listToShuffle.isEmpty()) return;
if (current >= 0) {
Song song = listToShuffle.remove(current);
Collections.shuffle(listToShuffle);
listToShuffle.add(0, song);
} else {
Collections.shuffle(listToShuffle);
}
}
}

View file

@ -0,0 +1,332 @@
package com.dkanada.gramophone.helper;
import android.graphics.Bitmap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Blur using Java code.
* <p/>
* This is a compromise between Gaussian Blur and Box blur
* It creates much better looking blurs than Box Blur, but is
* 7x faster than my Gaussian Blur implementation.
* <p/>
* I called it Stack Blur because this describes best how this
* filter works internally: it creates a kind of moving stack
* of colors whilst scanning through the image. Thereby it
* just has to add one new block of color to the right side
* of the stack and remove the leftmost color. The remaining
* colors on the topmost layer of the stack are either added on
* or reduced by one, depending on if they are on the right or
* on the left side of the stack.
*
* @author Enrique López Mañas <eenriquelopez@gmail.com>
* http://www.neo-tech.es
* <p/>
* Author of the original algorithm: Mario Klingemann <mario.quasimondo.com>
* <p/>
* Based heavily on http://vitiy.info/Code/stackblur.cpp
* See http://vitiy.info/stackblur-algorithm-multi-threaded-blur-for-cpp/
* @copyright: Enrique López Mañas
* @license: Apache License 2.0
*/
public class StackBlur {
static final int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors();
static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(EXECUTOR_THREADS);
private static final short[] stackblur_mul = {
512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512,
454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512,
482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456,
437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512,
497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328,
320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456,
446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335,
329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512,
505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405,
399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328,
324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271,
268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456,
451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,
385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335,
332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,
289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259
};
private static final byte[] stackblur_shr = {
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24
};
public static Bitmap blur(Bitmap original, float radius) {
int w = original.getWidth();
int h = original.getHeight();
int[] currentPixels = new int[w * h];
original.getPixels(currentPixels, 0, w, 0, 0, w, h);
int cores = EXECUTOR_THREADS;
List<BlurTask> horizontal = new ArrayList<>(cores);
List<BlurTask> vertical = new ArrayList<>(cores);
for (int i = 0; i < cores; i++) {
horizontal.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 1));
vertical.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 2));
}
try {
EXECUTOR.invokeAll(horizontal);
} catch (InterruptedException e) {
return null;
}
try {
EXECUTOR.invokeAll(vertical);
} catch (InterruptedException e) {
return null;
}
return Bitmap.createBitmap(currentPixels, w, h, Bitmap.Config.ARGB_8888);
}
private static void blurIteration(int[] src, int w, int h, int radius, int cores, int core, int step) {
int x, y, xp, yp, i;
int sp;
int stack_start;
int stack_i;
int src_i;
int dst_i;
long sum_r, sum_g, sum_b,
sum_in_r, sum_in_g, sum_in_b,
sum_out_r, sum_out_g, sum_out_b;
int wm = w - 1;
int hm = h - 1;
int div = (radius * 2) + 1;
int mul_sum = stackblur_mul[radius];
byte shr_sum = stackblur_shr[radius];
int[] stack = new int[div];
if (step == 1) {
int minY = core * h / cores;
int maxY = (core + 1) * h / cores;
for (y = minY; y < maxY; y++) {
sum_r = sum_g = sum_b =
sum_in_r = sum_in_g = sum_in_b =
sum_out_r = sum_out_g = sum_out_b = 0;
src_i = w * y; // start of line (0,y)
for (i = 0; i <= radius; i++) {
stack_i = i;
stack[stack_i] = src[src_i];
sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1);
sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1);
sum_b += (src[src_i] & 0xff) * (i + 1);
sum_out_r += ((src[src_i] >>> 16) & 0xff);
sum_out_g += ((src[src_i] >>> 8) & 0xff);
sum_out_b += (src[src_i] & 0xff);
}
for (i = 1; i <= radius; i++) {
if (i <= wm) src_i += 1;
stack_i = i + radius;
stack[stack_i] = src[src_i];
sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i);
sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i);
sum_b += (src[src_i] & 0xff) * (radius + 1 - i);
sum_in_r += ((src[src_i] >>> 16) & 0xff);
sum_in_g += ((src[src_i] >>> 8) & 0xff);
sum_in_b += (src[src_i] & 0xff);
}
sp = radius;
xp = radius;
if (xp > wm) xp = wm;
src_i = xp + y * w; // img.pix_ptr(xp, y);
dst_i = y * w; // img.pix_ptr(0, y);
for (x = 0; x < w; x++) {
src[dst_i] = (int)
((src[dst_i] & 0xff000000) |
((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) |
((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) |
((((sum_b * mul_sum) >>> shr_sum) & 0xff)));
dst_i += 1;
sum_r -= sum_out_r;
sum_g -= sum_out_g;
sum_b -= sum_out_b;
stack_start = sp + div - radius;
if (stack_start >= div) stack_start -= div;
stack_i = stack_start;
sum_out_r -= ((stack[stack_i] >>> 16) & 0xff);
sum_out_g -= ((stack[stack_i] >>> 8) & 0xff);
sum_out_b -= (stack[stack_i] & 0xff);
if (xp < wm) {
src_i += 1;
++xp;
}
stack[stack_i] = src[src_i];
sum_in_r += ((src[src_i] >>> 16) & 0xff);
sum_in_g += ((src[src_i] >>> 8) & 0xff);
sum_in_b += (src[src_i] & 0xff);
sum_r += sum_in_r;
sum_g += sum_in_g;
sum_b += sum_in_b;
++sp;
if (sp >= div) sp = 0;
stack_i = sp;
sum_out_r += ((stack[stack_i] >>> 16) & 0xff);
sum_out_g += ((stack[stack_i] >>> 8) & 0xff);
sum_out_b += (stack[stack_i] & 0xff);
sum_in_r -= ((stack[stack_i] >>> 16) & 0xff);
sum_in_g -= ((stack[stack_i] >>> 8) & 0xff);
sum_in_b -= (stack[stack_i] & 0xff);
}
}
}
// step 2
else if (step == 2) {
int minX = core * w / cores;
int maxX = (core + 1) * w / cores;
for (x = minX; x < maxX; x++) {
sum_r = sum_g = sum_b =
sum_in_r = sum_in_g = sum_in_b =
sum_out_r = sum_out_g = sum_out_b = 0;
src_i = x; // x,0
for (i = 0; i <= radius; i++) {
stack_i = i;
stack[stack_i] = src[src_i];
sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1);
sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1);
sum_b += (src[src_i] & 0xff) * (i + 1);
sum_out_r += ((src[src_i] >>> 16) & 0xff);
sum_out_g += ((src[src_i] >>> 8) & 0xff);
sum_out_b += (src[src_i] & 0xff);
}
for (i = 1; i <= radius; i++) {
if (i <= hm) src_i += w; // +stride
stack_i = i + radius;
stack[stack_i] = src[src_i];
sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i);
sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i);
sum_b += (src[src_i] & 0xff) * (radius + 1 - i);
sum_in_r += ((src[src_i] >>> 16) & 0xff);
sum_in_g += ((src[src_i] >>> 8) & 0xff);
sum_in_b += (src[src_i] & 0xff);
}
sp = radius;
yp = radius;
if (yp > hm) yp = hm;
src_i = x + yp * w; // img.pix_ptr(x, yp);
dst_i = x; // img.pix_ptr(x, 0);
for (y = 0; y < h; y++) {
src[dst_i] = (int)
((src[dst_i] & 0xff000000) |
((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) |
((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) |
((((sum_b * mul_sum) >>> shr_sum) & 0xff)));
dst_i += w;
sum_r -= sum_out_r;
sum_g -= sum_out_g;
sum_b -= sum_out_b;
stack_start = sp + div - radius;
if (stack_start >= div) stack_start -= div;
stack_i = stack_start;
sum_out_r -= ((stack[stack_i] >>> 16) & 0xff);
sum_out_g -= ((stack[stack_i] >>> 8) & 0xff);
sum_out_b -= (stack[stack_i] & 0xff);
if (yp < hm) {
src_i += w; // stride
++yp;
}
stack[stack_i] = src[src_i];
sum_in_r += ((src[src_i] >>> 16) & 0xff);
sum_in_g += ((src[src_i] >>> 8) & 0xff);
sum_in_b += (src[src_i] & 0xff);
sum_r += sum_in_r;
sum_g += sum_in_g;
sum_b += sum_in_b;
++sp;
if (sp >= div) sp = 0;
stack_i = sp;
sum_out_r += ((stack[stack_i] >>> 16) & 0xff);
sum_out_g += ((stack[stack_i] >>> 8) & 0xff);
sum_out_b += (stack[stack_i] & 0xff);
sum_in_r -= ((stack[stack_i] >>> 16) & 0xff);
sum_in_g -= ((stack[stack_i] >>> 8) & 0xff);
sum_in_b -= (stack[stack_i] & 0xff);
}
}
}
}
private static class BlurTask implements Callable<Void> {
private final int[] _src;
private final int _w;
private final int _h;
private final int _radius;
private final int _totalCores;
private final int _coreIndex;
private final int _round;
public BlurTask(int[] src, int w, int h, int radius, int totalCores, int coreIndex, int round) {
_src = src;
_w = w;
_h = h;
_radius = radius;
_totalCores = totalCores;
_coreIndex = coreIndex;
_round = round;
}
@Override
public Void call() throws Exception {
blurIteration(_src, _w, _h, _radius, _totalCores, _coreIndex, _round);
return null;
}
}
}

View file

@ -0,0 +1,47 @@
package com.dkanada.gramophone.helper;
import java.util.Locale;
public class StopWatch {
private long startTime;
private long previousElapsedTime;
private boolean isRunning;
public void start() {
synchronized (this) {
startTime = System.currentTimeMillis();
isRunning = true;
}
}
public void pause() {
synchronized (this) {
previousElapsedTime += System.currentTimeMillis() - startTime;
isRunning = false;
}
}
public void reset() {
synchronized (this) {
startTime = 0;
previousElapsedTime = 0;
isRunning = false;
}
}
public final long getElapsedTime() {
synchronized (this) {
long currentElapsedTime = 0;
if (isRunning) {
currentElapsedTime = System.currentTimeMillis() - startTime;
}
return previousElapsedTime + currentElapsedTime;
}
}
@Override
public String toString() {
return String.format(Locale.getDefault(), "%d millis", getElapsedTime());
}
}

View file

@ -0,0 +1,50 @@
package com.dkanada.gramophone.helper;
import android.graphics.Canvas;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
public class SwipeAndDragHelper extends ItemTouchHelper.Callback {
private ActionCompletionContract contract;
public SwipeAndDragHelper(ActionCompletionContract contract) {
this.contract = contract;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
contract.onViewMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float alpha = 1 - (Math.abs(dX) / recyclerView.getWidth());
viewHolder.itemView.setAlpha(alpha);
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
public interface ActionCompletionContract {
void onViewMoved(int oldPosition, int newPosition);
}
}

View file

@ -0,0 +1,48 @@
package com.dkanada.gramophone.helper.menu;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.dialogs.AddToPlaylistDialog;
import com.dkanada.gramophone.dialogs.DeletePlaylistDialog;
import com.dkanada.gramophone.dialogs.RenamePlaylistDialog;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.model.Song;
import java.util.ArrayList;
import java.util.List;
public class PlaylistMenuHelper {
public static boolean handleMenuClick(@NonNull AppCompatActivity activity, @NonNull final Playlist playlist, @NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_play:
MusicPlayerRemote.openQueue(new ArrayList<>(getPlaylistSongs(activity, playlist)), 0, true);
return true;
case R.id.action_play_next:
MusicPlayerRemote.playNext(new ArrayList<>(getPlaylistSongs(activity, playlist)));
return true;
case R.id.action_add_to_queue:
MusicPlayerRemote.enqueue(new ArrayList<>(getPlaylistSongs(activity, playlist)));
return true;
case R.id.action_add_to_playlist:
AddToPlaylistDialog.create(new ArrayList<>(getPlaylistSongs(activity, playlist))).show(activity.getSupportFragmentManager(), "ADD_PLAYLIST");
return true;
case R.id.action_rename_playlist:
RenamePlaylistDialog.create(playlist).show(activity.getSupportFragmentManager(), "RENAME_PLAYLIST");
return true;
case R.id.action_delete_playlist:
DeletePlaylistDialog.create(playlist).show(activity.getSupportFragmentManager(), "DELETE_PLAYLIST");
return true;
}
return false;
}
@NonNull
private static List<Song> getPlaylistSongs(@NonNull Activity activity, Playlist playlist) {
return new ArrayList<>();
}
}

View file

@ -0,0 +1,77 @@
package com.dkanada.gramophone.helper.menu;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem;
import android.view.View;
import android.widget.PopupMenu;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.dialogs.AddToPlaylistDialog;
import com.dkanada.gramophone.dialogs.SongDetailDialog;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
public class SongMenuHelper {
public static final int MENU_RES = R.menu.menu_item_song;
public static boolean handleMenuClick(@NonNull FragmentActivity activity, @NonNull Song song, int menuItemId) {
switch (menuItemId) {
case R.id.action_share:
activity.startActivity(Intent.createChooser(MusicUtil.createShareSongFileIntent(song, activity), null));
return true;
case R.id.action_add_to_playlist:
AddToPlaylistDialog.create(song).show(activity.getSupportFragmentManager(), "ADD_PLAYLIST");
return true;
case R.id.action_play_next:
MusicPlayerRemote.playNext(song);
return true;
case R.id.action_add_to_queue:
MusicPlayerRemote.enqueue(song);
return true;
case R.id.action_details:
SongDetailDialog.create(song).show(activity.getSupportFragmentManager(), "SONG_DETAILS");
return true;
case R.id.action_go_to_album:
NavigationUtil.goToAlbum(activity, new Album(song));
return true;
case R.id.action_go_to_artist:
NavigationUtil.goToArtist(activity, new Artist(song));
return true;
}
return false;
}
public static abstract class OnClickSongMenu implements View.OnClickListener, PopupMenu.OnMenuItemClickListener {
private AppCompatActivity activity;
public OnClickSongMenu(@NonNull AppCompatActivity activity) {
this.activity = activity;
}
public int getMenuRes() {
return MENU_RES;
}
@Override
public void onClick(View v) {
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.inflate(getMenuRes());
popupMenu.setOnMenuItemClickListener(this);
popupMenu.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
return handleMenuClick(activity, getSong(), item.getItemId());
}
public abstract Song getSong();
}
}

View file

@ -0,0 +1,29 @@
package com.dkanada.gramophone.helper.menu;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.dialogs.AddToPlaylistDialog;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.model.Song;
import java.util.List;
public class SongsMenuHelper {
public static boolean handleMenuClick(@NonNull FragmentActivity activity, @NonNull List<Song> songs, int menuItemId) {
switch (menuItemId) {
case R.id.action_play_next:
MusicPlayerRemote.playNext(songs);
return true;
case R.id.action_add_to_queue:
MusicPlayerRemote.enqueue(songs);
return true;
case R.id.action_add_to_playlist:
AddToPlaylistDialog.create(songs).show(activity.getSupportFragmentManager(), "ADD_PLAYLIST");
return true;
}
return false;
}
}

View file

@ -0,0 +1,9 @@
package com.dkanada.gramophone.helper.sort;
public class SortMethod {
public static final String NAME = "NAME";
public static final String ALBUM = "ALBUM";
public static final String ARTIST = "ARTIST";
public static final String YEAR = "YEAR";
public static final String RANDOM = "RANDOM";
}

View file

@ -0,0 +1,6 @@
package com.dkanada.gramophone.helper.sort;
public class SortOrder {
public static final String ASCENDING = "ASCENDING";
public static final String DESCENDING = "DESCENDING";
}

View file

@ -0,0 +1,11 @@
package com.dkanada.gramophone.interfaces;
import androidx.annotation.NonNull;
import com.afollestad.materialcab.MaterialCab;
public interface CabHolder {
@NonNull
MaterialCab openCab(final int menuRes, final MaterialCab.Callback callback);
}

View file

@ -0,0 +1,7 @@
package com.dkanada.gramophone.interfaces;
import java.util.List;
public interface MediaCallback {
void onLoadMedia(List<?> media);
}

View file

@ -0,0 +1,19 @@
package com.dkanada.gramophone.interfaces;
public interface MusicServiceEventListener {
void onServiceConnected();
void onServiceDisconnected();
void onQueueChanged();
void onPlayingMetaChanged();
void onPlayStateChanged();
void onRepeatModeChanged();
void onShuffleModeChanged();
void onMediaStoreChanged();
}

View file

@ -0,0 +1,9 @@
package com.dkanada.gramophone.interfaces;
import androidx.annotation.ColorInt;
public interface PaletteColorHolder {
@ColorInt
int getPaletteColor();
}

View file

@ -0,0 +1,111 @@
package com.dkanada.gramophone.loader;
import android.content.Context;
import android.database.Cursor;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.util.PreferenceUtil;
import java.util.ArrayList;
import java.util.List;
public class SongLoader {
public static final String QUEUE_PRIMARY = "image";
public static final String QUEUE_FAVORITE = "favorite";
protected static final String BASE_SELECTION = AudioColumns.IS_MUSIC + "=1" + " AND " + AudioColumns.TITLE + " != ''";
protected static final String[] BASE_PROJECTION = new String[]{
BaseColumns._ID,
AudioColumns.TITLE,
AudioColumns.TRACK,
AudioColumns.YEAR,
AudioColumns.DURATION,
AudioColumns.ALBUM_ID,
AudioColumns.ALBUM,
AudioColumns.ARTIST_ID,
AudioColumns.ARTIST,
QUEUE_PRIMARY,
QUEUE_FAVORITE,
};
@NonNull
public static List<Song> getAllSongs(@NonNull Context context) {
Cursor cursor = makeSongCursor(context, null, null);
return getSongs(cursor);
}
@NonNull
public static List<Song> getSongs(@Nullable final Cursor cursor) {
List<Song> songs = new ArrayList<>();
if (cursor != null && cursor.moveToFirst()) {
do {
songs.add(getSongFromCursorImpl(cursor));
} while (cursor.moveToNext());
}
if (cursor != null) cursor.close();
return songs;
}
@NonNull
public static Song getSong(@Nullable Cursor cursor) {
Song song;
if (cursor != null && cursor.moveToFirst()) {
song = getSongFromCursorImpl(cursor);
} else {
song = Song.EMPTY_SONG;
}
if (cursor != null) {
cursor.close();
}
return song;
}
@NonNull
private static Song getSongFromCursorImpl(@NonNull Cursor cursor) {
final String id = cursor.getString(0);
final String title = cursor.getString(1);
final int trackNumber = cursor.getInt(2);
final int year = cursor.getInt(3);
final long duration = cursor.getLong(4);
final String albumId = cursor.getString(5);
final String albumName = cursor.getString(6);
final String artistId = cursor.getString(7);
final String artistName = cursor.getString(8);
final String primary = cursor.getString(9);
final boolean favorite = Boolean.valueOf(cursor.getString(10));
return new Song(id, title, trackNumber, year, duration, albumId, albumName, artistId, artistName, primary, favorite);
}
@Nullable
public static Cursor makeSongCursor(@NonNull final Context context, @Nullable final String selection, final String[] selectionValues) {
return makeSongCursor(context, selection, selectionValues, PreferenceUtil.getInstance(context).getSongSortOrder());
}
@Nullable
public static Cursor makeSongCursor(@NonNull final Context context, @Nullable String selection, String[] selectionValues, final String sortOrder) {
if (selection != null && !selection.trim().equals("")) {
selection = BASE_SELECTION + " AND " + selection;
} else {
selection = BASE_SELECTION;
}
try {
return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
BASE_PROJECTION, selection, selectionValues, sortOrder);
} catch (SecurityException e) {
return null;
}
}
}

View file

@ -0,0 +1,239 @@
package com.dkanada.gramophone.misc;
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.
*/
import android.os.Bundle;
import android.os.Parcelable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of {@link PagerAdapter} that
* uses a {@link Fragment} to manage each page. This class also handles
* saving and restoring of fragment's state.
* <p/>
* <p>This version of the pager is more useful when there are a large number
* of pages, working more like a list view. When pages are not visible to
* the user, their entire fragment may be destroyed, only keeping the saved
* state of that fragment. This allows the pager to hold on to much less
* memory associated with each visited page as compared to
* {@link FragmentPagerAdapter} at the cost of potentially more overhead when
* switching between pages.
* <p/>
* <p>When using FragmentPagerAdapter the host ViewPager must have a
* valid ID set.</p>
* <p/>
* <p>Subclasses only need to implement {@link #getItem(int)}
* and {@link #getCount()} to have a working adapter.
* <p/>
* <p>Here is an example implementation of a pager containing fragments of
* lists:
* <p/>
* {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java
* complete}
* <p/>
* <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is:
* <p/>
* {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml
* complete}
* <p/>
* <p>The <code>R.layout.fragment_pager_list</code> resource containing each
* individual fragment's layout is:
* <p/>
* {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml
* complete}
*/
public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
public static final String TAG = CustomFragmentStatePagerAdapter.class.getSimpleName();
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private List<Fragment.SavedState> mSavedState = new ArrayList<>();
private List<Fragment> mFragments = new ArrayList<>();
private Fragment mCurrentPrimaryItem = null;
public CustomFragmentStatePagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(ViewGroup container) {
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment) object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
mFragments.set(position, null);
mCurTransaction.remove(fragment);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment) object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i = 0; i < mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (Parcelable fs : fss) {
mSavedState.add((Fragment.SavedState) fs);
}
}
Iterable<String> keys = bundle.keySet();
for (String key : keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
f.setMenuVisibility(false);
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
public Fragment getFragment(int position) {
if (position < mFragments.size() && position >= 0) {
return mFragments.get(position);
}
return null;
}
}

View file

@ -0,0 +1,89 @@
package com.dkanada.gramophone.misc;
import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
public abstract class DialogAsyncTask<Params, Progress, Result> extends WeakContextAsyncTask<Params, Progress, Result> {
private final int delay;
private WeakReference<Dialog> dialogWeakReference;
private boolean supposedToBeDismissed;
public DialogAsyncTask(Context context) {
this(context, 0);
}
public DialogAsyncTask(Context context, int showDelay) {
super(context);
this.delay = showDelay;
dialogWeakReference = new WeakReference<>(null);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
if (delay > 0) {
new Handler().postDelayed(this::initAndShowDialog, delay);
} else {
initAndShowDialog();
}
}
private void initAndShowDialog() {
Context context = getContext();
if (!supposedToBeDismissed && context != null) {
Dialog dialog = createDialog(context);
dialogWeakReference = new WeakReference<>(dialog);
dialog.show();
}
}
@SuppressWarnings("unchecked")
@Override
protected void onProgressUpdate(Progress... values) {
super.onProgressUpdate(values);
Dialog dialog = getDialog();
if (dialog != null) {
onProgressUpdate(dialog, values);
}
}
@SuppressWarnings("unchecked")
protected void onProgressUpdate(@NonNull Dialog dialog, Progress... values) {
}
@Nullable
protected Dialog getDialog() {
return dialogWeakReference.get();
}
@Override
protected void onCancelled(Result result) {
super.onCancelled(result);
tryToDismiss();
}
@Override
protected void onPostExecute(Result result) {
super.onPostExecute(result);
tryToDismiss();
}
private void tryToDismiss() {
supposedToBeDismissed = true;
try {
Dialog dialog = getDialog();
if (dialog != null)
dialog.dismiss();
} catch (Exception e) {
e.printStackTrace();
}
}
protected abstract Dialog createDialog(@NonNull Context context);
}

View file

@ -0,0 +1,25 @@
package com.dkanada.gramophone.misc;
import android.animation.Animator;
public abstract class SimpleAnimatorListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}

View file

@ -0,0 +1,18 @@
package com.dkanada.gramophone.misc;
import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks;
import com.github.ksoichiro.android.observablescrollview.ScrollState;
public abstract class SimpleObservableScrollViewCallbacks implements ObservableScrollViewCallbacks {
@Override
public void onScrollChanged(int i, boolean b, boolean b2) {
}
@Override
public void onDownMotionEvent() {
}
@Override
public void onUpOrCancelMotionEvent(ScrollState scrollState) {
}
}

View file

@ -0,0 +1,17 @@
package com.dkanada.gramophone.misc;
import android.widget.SeekBar;
public abstract class SimpleOnSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}

View file

@ -0,0 +1,20 @@
package com.dkanada.gramophone.misc;
import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
public abstract class WeakContextAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
private WeakReference<Context> contextWeakReference;
public WeakContextAsyncTask(Context context) {
contextWeakReference = new WeakReference<>(context);
}
@Nullable
protected Context getContext() {
return contextWeakReference.get();
}
}

View file

@ -0,0 +1,72 @@
package com.dkanada.gramophone.misc;
import android.content.Context;
import androidx.loader.content.AsyncTaskLoader;
/**
* <a href="http://code.google.com/p/android/issues/detail?id=14944">Issue
* 14944</a>
*
* @author Alexander Blom
*/
public abstract class WrappedAsyncTaskLoader<D> extends AsyncTaskLoader<D> {
private D mData;
/**
* Constructor of <code>WrappedAsyncTaskLoader</code>
*
* @param context The {@link Context} to use.
*/
public WrappedAsyncTaskLoader(Context context) {
super(context);
}
/**
* {@inheritDoc}
*/
@Override
public void deliverResult(D data) {
if (!isReset()) {
this.mData = data;
super.deliverResult(data);
} else {
// An asynchronous query came in while the loader is stopped
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onStartLoading() {
super.onStartLoading();
if (this.mData != null) {
deliverResult(this.mData);
} else if (takeContentChanged() || this.mData == null) {
forceLoad();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onStopLoading() {
super.onStopLoading();
// Attempt to cancel the current load task if possible
cancelLoad();
}
/**
* {@inheritDoc}
*/
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
this.mData = null;
}
}

View file

@ -0,0 +1,142 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import com.dkanada.gramophone.App;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.ImageOptions;
import org.jellyfin.apiclient.model.entities.ImageType;
import java.util.ArrayList;
import java.util.List;
public class Album implements Parcelable {
public List<Song> songs;
public String id;
public String title;
public int year;
public String artistId;
public String artistName;
public String primary;
public Album(BaseItemDto itemDto) {
this.id = itemDto.getId();
this.title = itemDto.getName();
this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
if (itemDto.getAlbumArtists().size() != 0) {
this.artistId = itemDto.getAlbumArtists().get(0).getId();
this.artistName = itemDto.getAlbumArtists().get(0).getName();
} else if (itemDto.getArtistItems().size() != 0) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
}
this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
this.songs = new ArrayList<>();
}
public Album(Song song) {
this.id = song.albumId;
this.title = song.albumName;
this.year = song.year;
this.artistId = song.artistId;
this.artistName = song.artistName;
this.primary = song.primary;
}
public Album() {
this.songs = new ArrayList<>();
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getArtistId() {
return artistId;
}
public String getArtistName() {
return artistName;
}
public int getYear() {
return year;
}
public int getSongCount() {
return songs.size();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Album album = (Album) o;
return id.equals(album.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(title);
dest.writeInt(year);
dest.writeString(artistId);
dest.writeString(artistName);
dest.writeString(primary);
}
protected Album(Parcel in) {
this.songs = new ArrayList<>();
this.id = in.readString();
this.title = in.readString();
this.year = in.readInt();
this.artistId = in.readString();
this.artistName = in.readString();
this.primary = in.readString();
}
public static final Creator<Album> CREATOR = new Creator<Album>() {
public Album createFromParcel(Parcel source) {
return new Album(source);
}
public Album[] newArray(int size) {
return new Album[size];
}
};
}

View file

@ -0,0 +1,129 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.GenreDto;
import org.jellyfin.apiclient.model.entities.ImageType;
import java.util.ArrayList;
import java.util.List;
public class Artist implements Parcelable {
public List<Genre> genres;
public List<Album> albums;
public List<Song> songs;
public String id;
public String name;
public String primary;
public Artist(BaseItemDto itemDto) {
this.id = itemDto.getId();
this.name = itemDto.getName();
this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
this.genres = new ArrayList<>();
this.albums = new ArrayList<>();
this.songs = new ArrayList<>();
if (itemDto.getGenreItems() != null) {
for (GenreDto genre : itemDto.getGenreItems()) {
genres.add(new Genre(genre));
}
}
}
public Artist(Album album) {
this.id = album.artistId;
this.name = album.artistName;
}
public Artist(Song song) {
this.id = song.artistId;
this.name = song.artistName;
}
public Artist() {
this.albums = new ArrayList<>();
this.songs = new ArrayList<>();
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getSongCount() {
return songs.size();
}
public int getAlbumCount() {
return albums.size();
}
public List<Song> getSongs() {
return songs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Artist artist = (Artist) o;
return id.equals(artist.getId());
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(name);
dest.writeString(primary);
}
protected Artist(Parcel in) {
this.genres = new ArrayList<>();
this.albums = new ArrayList<>();
this.songs = new ArrayList<>();
this.id = in.readString();
this.name = in.readString();
this.primary = in.readString();
}
public static final Parcelable.Creator<Artist> CREATOR = new Parcelable.Creator<Artist>() {
@Override
public Artist createFromParcel(Parcel source) {
return new Artist(source);
}
@Override
public Artist[] newArray(int size) {
return new Artist[size];
}
};
}

View file

@ -0,0 +1,55 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import com.dkanada.gramophone.R;
public class CategoryInfo implements Parcelable {
public Category category;
public boolean visible;
public CategoryInfo(Category category, boolean visible) {
this.category = category;
this.visible = visible;
}
private CategoryInfo(Parcel source) {
category = (Category) source.readSerializable();
visible = source.readInt() == 1;
}
@Override
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(category);
dest.writeInt(visible ? 1 : 0);
}
public static final Parcelable.Creator<CategoryInfo> CREATOR = new Parcelable.Creator<CategoryInfo>() {
public CategoryInfo createFromParcel(Parcel source) {
return new CategoryInfo(source);
}
public CategoryInfo[] newArray(int size) {
return new CategoryInfo[size];
}
};
public enum Category {
SONGS(R.string.songs),
ALBUMS(R.string.albums),
ARTISTS(R.string.artists),
GENRES(R.string.genres),
PLAYLISTS(R.string.playlists);
public final int stringRes;
Category(int stringRes) {
this.stringRes = stringRes;
}
}
}

View file

@ -0,0 +1,72 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.GenreDto;
public class Genre implements Parcelable {
public final String id;
public final String name;
public final int songCount;
public Genre(GenreDto genreDto) {
this.id = genreDto.getId();
this.name = genreDto.getName();
this.songCount = 0;
}
public Genre(BaseItemDto itemDto) {
this.id = itemDto.getId();
this.name = itemDto.getName();
this.songCount = itemDto.getSongCount() != null ? itemDto.getSongCount() : 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Genre genre = (Genre) o;
return id.equals(genre.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.id);
dest.writeString(this.name);
dest.writeInt(this.songCount);
}
protected Genre(Parcel in) {
this.id = in.readString();
this.name = in.readString();
this.songCount = in.readInt();
}
public static final Creator<Genre> CREATOR = new Creator<Genre>() {
public Genre createFromParcel(Parcel source) {
return new Genre(source);
}
public Genre[] newArray(int size) {
return new Genre[size];
}
};
}

View file

@ -0,0 +1,71 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
public class Playlist implements Parcelable {
public final String id;
public final String name;
public Playlist(BaseItemDto itemDto) {
this.id = itemDto.getId();
this.name = itemDto.getName();
}
public Playlist(final int id, final String name) {
this.id = Integer.toString(id);
this.name = name;
}
public Playlist() {
this.id = "";
this.name = "";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Playlist playlist = (Playlist) o;
return id.equals(playlist.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.id);
dest.writeString(this.name);
}
protected Playlist(Parcel in) {
this.id = in.readString();
this.name = in.readString();
}
public static final Creator<Playlist> CREATOR = new Creator<Playlist>() {
public Playlist createFromParcel(Parcel source) {
return new Playlist(source);
}
public Playlist[] newArray(int size) {
return new Playlist[size];
}
};
}

View file

@ -0,0 +1,42 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
public class PlaylistSong extends Song {
public final String playlistId;
public final String indexId;
public PlaylistSong(BaseItemDto itemDto, String playlist) {
super(itemDto);
this.playlistId = playlist;
this.indexId = itemDto.getPlaylistItemId();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(this.playlistId);
dest.writeString(this.indexId);
}
protected PlaylistSong(Parcel in) {
super(in);
this.playlistId = in.readString();
this.indexId = in.readString();
}
public static final Creator<PlaylistSong> CREATOR = new Creator<PlaylistSong>() {
public PlaylistSong createFromParcel(Parcel source) {
return new PlaylistSong(source);
}
public PlaylistSong[] newArray(int size) {
return new PlaylistSong[size];
}
};
}

View file

@ -0,0 +1,133 @@
package com.dkanada.gramophone.model;
import android.os.Parcel;
import android.os.Parcelable;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
public class Song implements Parcelable {
public static final Song EMPTY_SONG = new Song(null, "", -1, -1, -1, null, "", null, "", null, false);
public final String id;
public final String title;
public final int trackNumber;
public final int year;
public final long duration;
public final String albumId;
public final String albumName;
public String artistId;
public String artistName;
public String primary;
public boolean favorite;
public Song(BaseItemDto itemDto) {
this.id = itemDto.getId();
this.title = itemDto.getName();
this.trackNumber = itemDto.getIndexNumber() != null ? itemDto.getIndexNumber() : 0;
this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
this.duration = itemDto.getRunTimeTicks() != null ? itemDto.getRunTimeTicks() / 10000 : 0;
this.albumId = itemDto.getAlbumId();
this.albumName = itemDto.getAlbum();
if (itemDto.getAlbumArtists().size() != 0) {
this.artistId = itemDto.getAlbumArtists().get(0).getId();
this.artistName = itemDto.getAlbumArtists().get(0).getName();
} else if (itemDto.getArtistItems().size() != 0) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
}
this.primary = itemDto.getAlbumPrimaryImageTag() != null ? albumId : null;
this.favorite = itemDto.getUserData() != null && itemDto.getUserData().getIsFavorite();
}
public Song(String id, String title, int trackNumber, int year, long duration, String albumId, String albumName, String artistId, String artistName, String primary, boolean favorite) {
this.id = id;
this.title = title;
this.trackNumber = trackNumber;
this.year = year;
this.duration = duration;
this.albumId = albumId;
this.albumName = albumName;
this.artistId = artistId;
this.artistName = artistName;
this.primary = primary;
this.favorite = favorite;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Song song = (Song) o;
return id.equals(song.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.id);
dest.writeString(this.title);
dest.writeInt(this.trackNumber);
dest.writeInt(this.year);
dest.writeLong(this.duration);
dest.writeString(this.albumId);
dest.writeString(this.albumName);
dest.writeString(this.artistId);
dest.writeString(this.artistName);
dest.writeString(this.primary);
dest.writeString(Boolean.toString(favorite));
}
protected Song(Parcel in) {
this.id = in.readString();
this.title = in.readString();
this.trackNumber = in.readInt();
this.year = in.readInt();
this.duration = in.readLong();
this.albumId = in.readString();
this.albumName = in.readString();
this.artistId = in.readString();
this.artistName = in.readString();
this.primary = in.readString();
this.favorite = Boolean.valueOf(in.readString());
}
public static final Creator<Song> CREATOR = new Creator<Song>() {
public Song createFromParcel(Parcel source) {
return new Song(source);
}
public Song[] newArray(int size) {
return new Song[size];
}
};
}

View file

@ -0,0 +1,24 @@
package com.dkanada.gramophone.preferences;
import android.content.Context;
import android.util.AttributeSet;
import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEDialogPreference;
public class LibraryPreference extends ATEDialogPreference {
public LibraryPreference(Context context) {
super(context);
}
public LibraryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LibraryPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LibraryPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}

View file

@ -0,0 +1,84 @@
package com.dkanada.gramophone.preferences;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import com.afollestad.materialdialogs.MaterialDialog;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.CategoryInfoAdapter;
import com.dkanada.gramophone.model.CategoryInfo;
import com.dkanada.gramophone.util.PreferenceUtil;
import java.util.ArrayList;
import java.util.List;
public class LibraryPreferenceDialog extends DialogFragment {
public static LibraryPreferenceDialog newInstance() {
return new LibraryPreferenceDialog();
}
private CategoryInfoAdapter adapter;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.preference_dialog_library_categories, null);
List<CategoryInfo> categoryInfos;
if (savedInstanceState != null) {
categoryInfos = savedInstanceState.getParcelableArrayList(PreferenceUtil.LIBRARY_CATEGORIES);
} else {
categoryInfos = PreferenceUtil.getInstance(getContext()).getLibraryCategories();
}
adapter = new CategoryInfoAdapter(categoryInfos);
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
adapter.attachToRecyclerView(recyclerView);
return new MaterialDialog.Builder(getContext())
.title(R.string.library_categories)
.customView(view, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.neutralText(R.string.reset_action)
.autoDismiss(false)
.onNeutral((dialog, action) -> adapter.setCategoryInfos(PreferenceUtil.getInstance(getContext()).getDefaultLibraryCategories()))
.onNegative((dialog, action) -> dismiss())
.onPositive((dialog, action) -> {
updateCategories(adapter.getCategoryInfos());
dismiss();
})
.build();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(PreferenceUtil.LIBRARY_CATEGORIES, new ArrayList<>(adapter.getCategoryInfos()));
}
private void updateCategories(List<CategoryInfo> categories) {
if (getSelected(categories) == 0) return;
PreferenceUtil.getInstance(getContext()).setLibraryCategories(categories);
}
private int getSelected(List<CategoryInfo> categories) {
int selected = 0;
for (CategoryInfo categoryInfo : categories) {
if (categoryInfo.visible)
selected++;
}
return selected;
}
}

View file

@ -0,0 +1,24 @@
package com.dkanada.gramophone.preferences;
import android.content.Context;
import android.util.AttributeSet;
import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEDialogPreference;
public class NowPlayingScreenPreference extends ATEDialogPreference {
public NowPlayingScreenPreference(Context context) {
super(context);
}
public NowPlayingScreenPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NowPlayingScreenPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public NowPlayingScreenPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}

View file

@ -0,0 +1,128 @@
package com.dkanada.gramophone.preferences;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.heinrichreimersoftware.materialintro.view.InkPageIndicator;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.ui.fragments.player.NowPlayingScreen;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.dkanada.gramophone.util.ViewUtil;
public class NowPlayingScreenPreferenceDialog extends DialogFragment implements MaterialDialog.SingleButtonCallback, ViewPager.OnPageChangeListener {
private DialogAction whichButtonClicked;
private int viewPagerPosition;
public static NowPlayingScreenPreferenceDialog newInstance() {
return new NowPlayingScreenPreferenceDialog();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
@SuppressLint("InflateParams") View view = LayoutInflater.from(getContext()).inflate(R.layout.preference_dialog_now_playing_screen, null);
ViewPager viewPager = view.findViewById(R.id.now_playing_screen_view_pager);
viewPager.setAdapter(new NowPlayingScreenAdapter(getContext()));
viewPager.addOnPageChangeListener(this);
viewPager.setPageMargin((int) ViewUtil.convertDpToPixel(32, getResources()));
viewPager.setCurrentItem(PreferenceUtil.getInstance(getContext()).getNowPlayingScreen().ordinal());
InkPageIndicator pageIndicator = view.findViewById(R.id.page_indicator);
pageIndicator.setViewPager(viewPager);
pageIndicator.onPageSelected(viewPager.getCurrentItem());
return new MaterialDialog.Builder(getContext())
.title(R.string.pref_title_now_playing_screen_appearance)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onAny(this)
.customView(view, false)
.build();
}
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
whichButtonClicked = which;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (whichButtonClicked == DialogAction.POSITIVE) {
PreferenceUtil.getInstance(getContext()).setNowPlayingScreen(NowPlayingScreen.values()[viewPagerPosition]);
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
this.viewPagerPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
}
private static class NowPlayingScreenAdapter extends PagerAdapter {
private Context context;
public NowPlayingScreenAdapter(Context context) {
this.context = context;
}
@Override
@NonNull
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
NowPlayingScreen nowPlayingScreen = NowPlayingScreen.values()[position];
LayoutInflater inflater = LayoutInflater.from(context);
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.preference_now_playing_screen_item, collection, false);
collection.addView(layout);
ImageView image = layout.findViewById(R.id.image);
TextView title = layout.findViewById(R.id.title);
image.setImageResource(nowPlayingScreen.drawableResId);
title.setText(nowPlayingScreen.titleRes);
return layout;
}
@Override
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
collection.removeView((View) view);
}
@Override
public int getCount() {
return NowPlayingScreen.values().length;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@Override
public CharSequence getPageTitle(int position) {
return context.getString(NowPlayingScreen.values()[position].titleRes);
}
}
}

View file

@ -0,0 +1,182 @@
/*
* Copyright (C) 2014 The CyanogenMod Project
*
* 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 com.dkanada.gramophone.provider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.provider.MediaStore.Audio.AudioColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.dkanada.gramophone.loader.SongLoader;
import com.dkanada.gramophone.model.Song;
import java.util.List;
public class QueueStore extends SQLiteOpenHelper {
@Nullable
private static QueueStore sInstance = null;
public static final String DATABASE_NAME = "music_playback_state.db";
public static final String PLAYING_QUEUE_TABLE_NAME = "playing_queue";
public static final String ORIGINAL_PLAYING_QUEUE_TABLE_NAME = "original_playing_queue";
private static final int VERSION = 5;
public QueueStore(final Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
@Override
public void onCreate(@NonNull final SQLiteDatabase db) {
createTable(db, PLAYING_QUEUE_TABLE_NAME);
createTable(db, ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
}
private void createTable(@NonNull final SQLiteDatabase db, final String tableName) {
// noinspection StringBufferReplaceableByString
StringBuilder builder = new StringBuilder();
builder.append("CREATE TABLE IF NOT EXISTS ");
builder.append(tableName);
builder.append("(");
builder.append(BaseColumns._ID);
builder.append(" INT NOT NULL,");
builder.append(AudioColumns.TITLE);
builder.append(" STRING NOT NULL,");
builder.append(AudioColumns.TRACK);
builder.append(" INT NOT NULL,");
builder.append(AudioColumns.YEAR);
builder.append(" INT NOT NULL,");
builder.append(AudioColumns.DURATION);
builder.append(" LONG NOT NULL,");
builder.append(AudioColumns.ALBUM_ID);
builder.append(" INT NOT NULL,");
builder.append(AudioColumns.ALBUM);
builder.append(" STRING NOT NULL,");
builder.append(AudioColumns.ARTIST_ID);
builder.append(" INT NOT NULL,");
builder.append(AudioColumns.ARTIST);
builder.append(" STRING NOT NULL,");
builder.append(SongLoader.QUEUE_PRIMARY);
builder.append(" STRING NOT NULL,");
builder.append(SongLoader.QUEUE_FAVORITE);
builder.append(" STRING NOT NULL);");
db.execSQL(builder.toString());
}
@Override
public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
onCreate(db);
}
@Override
public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
onCreate(db);
}
@NonNull
public static synchronized QueueStore getInstance(@NonNull final Context context) {
if (sInstance == null) {
sInstance = new QueueStore(context.getApplicationContext());
}
return sInstance;
}
public synchronized void saveQueues(@NonNull final List<Song> playingQueue, @NonNull final List<Song> originalPlayingQueue) {
saveQueue(PLAYING_QUEUE_TABLE_NAME, playingQueue);
saveQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME, originalPlayingQueue);
}
private synchronized void saveQueue(final String tableName, @NonNull final List<Song> queue) {
final SQLiteDatabase database = getWritableDatabase();
database.beginTransaction();
try {
database.delete(tableName, null, null);
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
final int NUM_PROCESS = 20;
int position = 0;
while (position < queue.size()) {
database.beginTransaction();
try {
for (int i = position; i < queue.size() && i < position + NUM_PROCESS; i++) {
Song song = queue.get(i);
ContentValues values = new ContentValues(4);
values.put(BaseColumns._ID, song.id);
values.put(AudioColumns.TITLE, song.title);
values.put(AudioColumns.TRACK, song.trackNumber);
values.put(AudioColumns.YEAR, song.year);
values.put(AudioColumns.DURATION, song.duration);
values.put(AudioColumns.ALBUM_ID, song.albumId);
values.put(AudioColumns.ALBUM, song.albumName);
values.put(AudioColumns.ARTIST_ID, song.artistId);
values.put(AudioColumns.ARTIST, song.artistName);
values.put(SongLoader.QUEUE_PRIMARY, song.primary);
values.put(SongLoader.QUEUE_FAVORITE, song.favorite);
database.insert(tableName, null, values);
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
position += NUM_PROCESS;
}
}
}
@NonNull
public List<Song> getSavedPlayingQueue() {
return getQueue(PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
public List<Song> getSavedOriginalPlayingQueue() {
return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
private List<Song> getQueue(@NonNull final String tableName) {
Cursor cursor = getReadableDatabase().query(tableName, null,
null, null, null, null, null);
return SongLoader.getSongs(cursor);
}
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (C) 2007 The Android Open Source Project 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.
*/
// Modified for Phonograph by Karim Abou Zeid (kabouzeid).
package com.dkanada.gramophone.service;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.view.KeyEvent;
import androidx.core.content.ContextCompat;
import com.dkanada.gramophone.BuildConfig;
/**
* Used to control headset playback.
* Single press: pause/resume
* Double press: next track
* Triple press: previous track
*/
public class MediaButtonIntentReceiver extends BroadcastReceiver {
private static final boolean DEBUG = BuildConfig.DEBUG;
public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
private static final int MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2;
private static final int DOUBLE_CLICK = 400;
private static WakeLock mWakeLock = null;
private static int mClickCounter = 0;
private static long mLastClickTime = 0;
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case MSG_HEADSET_DOUBLE_CLICK_TIMEOUT:
final int clickCount = msg.arg1;
final String command;
if (DEBUG) Log.v(TAG, "Handling headset click, count = " + clickCount);
switch (clickCount) {
case 1:
command = MusicService.ACTION_TOGGLE_PAUSE;
break;
case 2:
command = MusicService.ACTION_SKIP;
break;
case 3:
command = MusicService.ACTION_REWIND;
break;
default:
command = null;
break;
}
if (command != null) {
final Context context = (Context) msg.obj;
startService(context, command);
}
break;
}
releaseWakeLockIfHandlerIdle();
}
};
@Override
public void onReceive(final Context context, final Intent intent) {
if (DEBUG) Log.v(TAG, "Received intent: " + intent);
if (handleIntent(context, intent) && isOrderedBroadcast()) {
abortBroadcast();
}
}
public static boolean handleIntent(final Context context, final Intent intent) {
final String intentAction = intent.getAction();
if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event == null) {
return false;
}
final int keycode = event.getKeyCode();
final int action = event.getAction();
// fallback to system time if event time is not available
final long eventTime = event.getEventTime() != 0
? event.getEventTime()
: System.currentTimeMillis();
String command = null;
switch (keycode) {
case KeyEvent.KEYCODE_MEDIA_STOP:
command = MusicService.ACTION_STOP;
break;
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
command = MusicService.ACTION_TOGGLE_PAUSE;
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
command = MusicService.ACTION_SKIP;
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
command = MusicService.ACTION_REWIND;
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
command = MusicService.ACTION_PAUSE;
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
command = MusicService.ACTION_PLAY;
break;
}
if (command != null) {
if (action == KeyEvent.ACTION_DOWN) {
if (event.getRepeatCount() == 0) {
// Only consider the first event in a sequence, not the repeat events,
// so that we don't trigger in cases where the first event went to
// a different app (e.g. when the user ends a phone call by
// long pressing the headset button)
// The service may or may not be running, but we need to send it
// a command.
if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
if (eventTime - mLastClickTime >= DOUBLE_CLICK) {
mClickCounter = 0;
}
mClickCounter++;
if (DEBUG) Log.v(TAG, "Got headset click, count = " + mClickCounter);
mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT);
Message msg = mHandler.obtainMessage(
MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context);
long delay = mClickCounter < 3 ? DOUBLE_CLICK : 0;
if (mClickCounter >= 3) {
mClickCounter = 0;
}
mLastClickTime = eventTime;
acquireWakeLockAndSendMessage(context, msg, delay);
} else {
startService(context, command);
}
return true;
}
}
}
}
return false;
}
private static void startService(Context context, String command) {
final Intent intent = new Intent(context, MusicService.class);
intent.setAction(command);
try {
// IMPORTANT NOTE: (kind of a hack)
// on Android O and above the following crashes when the app is not running
// there is no good way to check whether the app is running so we catch the exception
// we do not always want to use startForegroundService() because then one gets an ANR
// if no notification is displayed via startForeground()
// according to Play analytics this happens a lot, I suppose for example if command = PAUSE
context.startService(intent);
} catch (IllegalStateException ignored) {
ContextCompat.startForegroundService(context, intent);
}
}
private static void acquireWakeLockAndSendMessage(Context context, Message msg, long delay) {
if (mWakeLock == null) {
Context appContext = context.getApplicationContext();
PowerManager pm = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Phonograph headset button");
mWakeLock.setReferenceCounted(false);
}
if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what);
mWakeLock.acquire(10000);
mHandler.sendMessageDelayed(msg, delay);
}
private static void releaseWakeLockIfHandlerIdle() {
if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock");
return;
}
if (mWakeLock != null) {
if (DEBUG) Log.v(TAG, "Releasing wake lock");
mWakeLock.release();
mWakeLock = null;
}
}
}

View file

@ -0,0 +1,284 @@
package com.dkanada.gramophone.service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.audiofx.AudioEffect;
import android.net.Uri;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.service.playback.Playback;
import com.dkanada.gramophone.util.PreferenceUtil;
public class MultiPlayer implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
public static final String TAG = MultiPlayer.class.getSimpleName();
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
private MediaPlayer mNextMediaPlayer;
private Context context;
@Nullable
private Playback.PlaybackCallbacks callbacks;
private boolean mIsInitialized = false;
public MultiPlayer(final Context context) {
this.context = context;
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
}
/**
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
* @return True if the <code>player</code> has been prepared and is
* ready to play, false otherwise
*/
@Override
public boolean setDataSource(@NonNull final String path) {
mIsInitialized = false;
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
if (mIsInitialized) {
setNextDataSource(null);
}
return mIsInitialized;
}
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
* @return True if the <code>player</code> has been prepared and is
* ready to play, false otherwise
*/
private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) {
if (context == null) {
return false;
}
try {
player.reset();
player.setOnPreparedListener(null);
if (path.startsWith("content://")) {
player.setDataSource(context, Uri.parse(path));
} else {
player.setDataSource(path);
}
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.prepare();
} catch (Exception e) {
return false;
}
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
context.sendBroadcast(intent);
return true;
}
/**
* Set the MediaPlayer to start when this MediaPlayer finishes playback.
*
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
*/
@Override
public void setNextDataSource(@Nullable final String path) {
if (context == null) {
return;
}
try {
mCurrentMediaPlayer.setNextMediaPlayer(null);
} catch (IllegalArgumentException e) {
Log.i(TAG, "Next media player is current one, continuing");
} catch (IllegalStateException e) {
Log.e(TAG, "Media player not initialized!");
return;
}
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
if (path == null) {
return;
}
if (PreferenceUtil.getInstance(context).getGaplessPlayback()) {
mNextMediaPlayer = new MediaPlayer();
mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
if (setDataSourceImpl(mNextMediaPlayer, path)) {
try {
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e);
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
}
} else {
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
}
}
}
@Override
public void setCallbacks(@Nullable Playback.PlaybackCallbacks callbacks) {
this.callbacks = callbacks;
}
@Override
public boolean isInitialized() {
return mIsInitialized;
}
@Override
public boolean start() {
try {
mCurrentMediaPlayer.start();
return true;
} catch (IllegalStateException e) {
return false;
}
}
@Override
public void stop() {
mCurrentMediaPlayer.reset();
mIsInitialized = false;
}
@Override
public void release() {
stop();
mCurrentMediaPlayer.release();
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
}
}
@Override
public boolean pause() {
try {
mCurrentMediaPlayer.pause();
return true;
} catch (IllegalStateException e) {
return false;
}
}
@Override
public boolean isPlaying() {
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
}
@Override
public int duration() {
if (!mIsInitialized) {
return -1;
}
try {
return mCurrentMediaPlayer.getDuration();
} catch (IllegalStateException e) {
return -1;
}
}
@Override
public int position() {
if (!mIsInitialized) {
return -1;
}
try {
return mCurrentMediaPlayer.getCurrentPosition();
} catch (IllegalStateException e) {
return -1;
}
}
@Override
public int seek(final int whereto) {
try {
mCurrentMediaPlayer.seekTo(whereto);
return whereto;
} catch (IllegalStateException e) {
return -1;
}
}
@Override
public boolean setVolume(final float vol) {
try {
mCurrentMediaPlayer.setVolume(vol, vol);
return true;
} catch (IllegalStateException e) {
return false;
}
}
@Override
public boolean setAudioSessionId(final int sessionId) {
try {
mCurrentMediaPlayer.setAudioSessionId(sessionId);
return true;
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
return false;
}
}
@Override
public int getAudioSessionId() {
return mCurrentMediaPlayer.getAudioSessionId();
}
@Override
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
mIsInitialized = false;
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = new MediaPlayer();
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
if (context != null) {
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
}
return false;
}
@Override
public void onCompletion(final MediaPlayer mp) {
if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
mIsInitialized = false;
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = mNextMediaPlayer;
mIsInitialized = true;
mNextMediaPlayer = null;
if (callbacks != null) callbacks.onTrackWentToNext();
} else {
if (callbacks != null) callbacks.onTrackEnded();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,77 @@
package com.dkanada.gramophone.service.notification;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
import androidx.annotation.RequiresApi;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.service.MusicService;
import static android.content.Context.NOTIFICATION_SERVICE;
public abstract class PlayingNotification {
private static final int NOTIFICATION_ID = 1;
static final String NOTIFICATION_CHANNEL_ID = "playing_notification";
private static final int NOTIFY_MODE_FOREGROUND = 1;
private static final int NOTIFY_MODE_BACKGROUND = 0;
private int notifyMode = NOTIFY_MODE_BACKGROUND;
private NotificationManager notificationManager;
protected MusicService service;
boolean stopped;
public synchronized void init(MusicService service) {
this.service = service;
notificationManager = (NotificationManager) service.getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
}
public abstract void update();
public synchronized void stop() {
stopped = true;
service.stopForeground(true);
notificationManager.cancel(NOTIFICATION_ID);
}
void updateNotifyModeAndPostNotification(Notification notification) {
int newNotifyMode;
if (service.isPlaying()) {
newNotifyMode = NOTIFY_MODE_FOREGROUND;
} else {
newNotifyMode = NOTIFY_MODE_BACKGROUND;
}
if (notifyMode != newNotifyMode && newNotifyMode == NOTIFY_MODE_BACKGROUND) {
service.stopForeground(false);
}
if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
service.startForeground(NOTIFICATION_ID, notification);
} else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
notificationManager.notify(NOTIFICATION_ID, notification);
}
notifyMode = newNotifyMode;
}
@RequiresApi(26)
private void createNotificationChannel() {
NotificationChannel notificationChannel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
if (notificationChannel == null) {
notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, service.getString(R.string.playing_notification_name), NotificationManager.IMPORTANCE_LOW);
notificationChannel.setDescription(service.getString(R.string.playing_notification_description));
notificationChannel.enableLights(false);
notificationChannel.enableVibration(false);
notificationManager.createNotificationChannel(notificationChannel);
}
}
}

View file

@ -0,0 +1,183 @@
package com.dkanada.gramophone.service.notification;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import android.text.TextUtils;
import android.view.View;
import android.widget.RemoteViews;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.Target;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.service.MusicService;
import com.dkanada.gramophone.ui.activities.MainActivity;
import com.dkanada.gramophone.util.ImageUtil;
import com.dkanada.gramophone.util.ThemeUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
public class PlayingNotificationImpl extends PlayingNotification {
private Target<BitmapPaletteWrapper> target;
@Override
public synchronized void update() {
stopped = false;
final Song song = service.getCurrentSong();
final boolean isPlaying = service.isPlaying();
final RemoteViews notificationLayout = new RemoteViews(service.getPackageName(), R.layout.notification);
final RemoteViews notificationLayoutBig = new RemoteViews(service.getPackageName(), R.layout.notification_big);
if (TextUtils.isEmpty(song.title) && TextUtils.isEmpty(song.artistName)) {
notificationLayout.setViewVisibility(R.id.media_titles, View.INVISIBLE);
} else {
notificationLayout.setViewVisibility(R.id.media_titles, View.VISIBLE);
notificationLayout.setTextViewText(R.id.title, song.title);
notificationLayout.setTextViewText(R.id.text, song.artistName);
}
if (TextUtils.isEmpty(song.title) && TextUtils.isEmpty(song.artistName) && TextUtils.isEmpty(song.albumName)) {
notificationLayoutBig.setViewVisibility(R.id.media_titles, View.INVISIBLE);
} else {
notificationLayoutBig.setViewVisibility(R.id.media_titles, View.VISIBLE);
notificationLayoutBig.setTextViewText(R.id.title, song.title);
notificationLayoutBig.setTextViewText(R.id.text, song.artistName);
notificationLayoutBig.setTextViewText(R.id.text2, song.albumName);
}
linkButtons(notificationLayout, notificationLayoutBig);
Intent action = new Intent(service, MainActivity.class);
action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent clickIntent = PendingIntent.getActivity(service, 0, action, 0);
final PendingIntent deleteIntent = buildPendingIntent(service, MusicService.ACTION_QUIT, null);
final Notification notification = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(clickIntent)
.setDeleteIntent(deleteIntent)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContent(notificationLayout)
.setCustomBigContentView(notificationLayoutBig)
.setOngoing(isPlaying)
.build();
final int bigNotificationImageSize = service.getResources().getDimensionPixelSize(R.dimen.notification_big_image_size);
service.runOnUiThread(new Runnable() {
@Override
public void run() {
if (target != null) {
Glide.clear(target);
}
target = CustomGlideRequest.Builder.from(Glide.with(service), song.primary)
.generatePalette(service).build()
.into(new SimpleTarget<BitmapPaletteWrapper>(bigNotificationImageSize, bigNotificationImageSize) {
@Override
public void onResourceReady(BitmapPaletteWrapper resource, GlideAnimation<? super BitmapPaletteWrapper> glideAnimation) {
update(resource.getBitmap(), ThemeUtil.getColor(resource.getPalette(), Color.TRANSPARENT));
}
@Override
public void onLoadFailed(Exception e, Drawable errorDrawable) {
super.onLoadFailed(e, errorDrawable);
update(null, Color.WHITE);
}
private void update(@Nullable Bitmap bitmap, int bgColor) {
if (bitmap != null) {
notificationLayout.setImageViewBitmap(R.id.image, bitmap);
notificationLayoutBig.setImageViewBitmap(R.id.image, bitmap);
} else {
notificationLayout.setImageViewResource(R.id.image, R.drawable.default_album_art);
notificationLayoutBig.setImageViewResource(R.id.image, R.drawable.default_album_art);
}
if (!PreferenceUtil.getInstance(service).getColoredNotification()) {
bgColor = Color.WHITE;
}
setBackgroundColor(bgColor);
setNotificationContent(ColorUtil.isColorLight(bgColor));
// notification has been stopped before loading was finished
if (stopped) return;
updateNotifyModeAndPostNotification(notification);
}
private void setBackgroundColor(int color) {
notificationLayout.setInt(R.id.root, "setBackgroundColor", color);
notificationLayoutBig.setInt(R.id.root, "setBackgroundColor", color);
}
private void setNotificationContent(boolean dark) {
int primary = MaterialValueHelper.getPrimaryTextColor(service, dark);
int secondary = MaterialValueHelper.getSecondaryTextColor(service, dark);
Bitmap prev = ImageUtil.createBitmap(ImageUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, primary), 1.5f);
Bitmap next = ImageUtil.createBitmap(ImageUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, primary), 1.5f);
Bitmap playPause = ImageUtil.createBitmap(ImageUtil.getTintedVectorDrawable(service, isPlaying ? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp, primary), 1.5f);
notificationLayout.setTextColor(R.id.title, primary);
notificationLayout.setTextColor(R.id.text, secondary);
notificationLayout.setImageViewBitmap(R.id.action_prev, prev);
notificationLayout.setImageViewBitmap(R.id.action_next, next);
notificationLayout.setImageViewBitmap(R.id.action_play_pause, playPause);
notificationLayoutBig.setTextColor(R.id.title, primary);
notificationLayoutBig.setTextColor(R.id.text, secondary);
notificationLayoutBig.setTextColor(R.id.text2, secondary);
notificationLayoutBig.setImageViewBitmap(R.id.action_prev, prev);
notificationLayoutBig.setImageViewBitmap(R.id.action_next, next);
notificationLayoutBig.setImageViewBitmap(R.id.action_play_pause, playPause);
}
});
}
});
}
private void linkButtons(final RemoteViews notificationLayout, final RemoteViews notificationLayoutBig) {
PendingIntent pendingIntent;
final ComponentName serviceName = new ComponentName(service, MusicService.class);
// Previous track
pendingIntent = buildPendingIntent(service, MusicService.ACTION_REWIND, serviceName);
notificationLayout.setOnClickPendingIntent(R.id.action_prev, pendingIntent);
notificationLayoutBig.setOnClickPendingIntent(R.id.action_prev, pendingIntent);
// Play and pause
pendingIntent = buildPendingIntent(service, MusicService.ACTION_TOGGLE_PAUSE, serviceName);
notificationLayout.setOnClickPendingIntent(R.id.action_play_pause, pendingIntent);
notificationLayoutBig.setOnClickPendingIntent(R.id.action_play_pause, pendingIntent);
// Next track
pendingIntent = buildPendingIntent(service, MusicService.ACTION_SKIP, serviceName);
notificationLayout.setOnClickPendingIntent(R.id.action_next, pendingIntent);
notificationLayoutBig.setOnClickPendingIntent(R.id.action_next, pendingIntent);
}
private PendingIntent buildPendingIntent(Context context, final String action, final ComponentName serviceName) {
Intent intent = new Intent(action);
intent.setComponent(serviceName);
return PendingIntent.getService(context, 0, intent, 0);
}
}

View file

@ -0,0 +1,115 @@
package com.dkanada.gramophone.service.notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.palette.graphics.Palette;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.service.MusicService;
import com.dkanada.gramophone.ui.activities.MainActivity;
import com.dkanada.gramophone.util.PreferenceUtil;
import static com.dkanada.gramophone.service.MusicService.ACTION_REWIND;
import static com.dkanada.gramophone.service.MusicService.ACTION_SKIP;
import static com.dkanada.gramophone.service.MusicService.ACTION_TOGGLE_PAUSE;
public class PlayingNotificationImpl24 extends PlayingNotification {
@Override
public synchronized void update() {
stopped = false;
final Song song = service.getCurrentSong();
final boolean isPlaying = service.isPlaying();
final int playButtonResId = isPlaying
? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp;
Intent action = new Intent(service, MainActivity.class);
action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent clickIntent = PendingIntent.getActivity(service, 0, action, 0);
final ComponentName serviceName = new ComponentName(service, MusicService.class);
Intent intent = new Intent(MusicService.ACTION_QUIT);
intent.setComponent(serviceName);
final PendingIntent deleteIntent = PendingIntent.getService(service, 0, intent, 0);
final int bigNotificationImageSize = service.getResources().getDimensionPixelSize(R.dimen.notification_big_image_size);
service.runOnUiThread(() -> CustomGlideRequest.Builder
.from(Glide.with(service), song.primary)
.generatePalette(service).build()
.into(new SimpleTarget<BitmapPaletteWrapper>(bigNotificationImageSize, bigNotificationImageSize) {
@Override
public void onResourceReady(BitmapPaletteWrapper resource, GlideAnimation<? super BitmapPaletteWrapper> glideAnimation) {
Palette palette = resource.getPalette();
update(resource.getBitmap(), palette.getVibrantColor(palette.getMutedColor(Color.TRANSPARENT)));
}
@Override
public void onLoadFailed(Exception e, Drawable errorDrawable) {
update(null, Color.TRANSPARENT);
}
void update(Bitmap bitmap, int color) {
if (bitmap == null)
bitmap = BitmapFactory.decodeResource(service.getResources(), R.drawable.default_album_art);
NotificationCompat.Action playPauseAction = new NotificationCompat.Action(playButtonResId,
service.getString(R.string.action_play_pause),
retrievePlaybackAction(ACTION_TOGGLE_PAUSE));
NotificationCompat.Action previousAction = new NotificationCompat.Action(R.drawable.ic_skip_previous_white_24dp,
service.getString(R.string.action_previous),
retrievePlaybackAction(ACTION_REWIND));
NotificationCompat.Action nextAction = new NotificationCompat.Action(R.drawable.ic_skip_next_white_24dp,
service.getString(R.string.action_next),
retrievePlaybackAction(ACTION_SKIP));
NotificationCompat.Builder builder = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setSubText(song.albumName)
.setLargeIcon(bitmap)
.setContentIntent(clickIntent)
.setDeleteIntent(deleteIntent)
.setContentTitle(song.title)
.setContentText(song.artistName)
.setOngoing(isPlaying)
.setShowWhen(false)
.addAction(previousAction)
.addAction(playPauseAction)
.addAction(nextAction);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setStyle(new MediaStyle().setMediaSession(service.getMediaSession().getSessionToken()).setShowActionsInCompactView(0, 1, 2))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O && PreferenceUtil.getInstance(service).getColoredNotification())
builder.setColor(color);
}
// notification has been stopped before loading was finished
if (stopped) return;
updateNotifyModeAndPostNotification(builder.build());
}
}));
}
private PendingIntent retrievePlaybackAction(final String action) {
final ComponentName serviceName = new ComponentName(service, MusicService.class);
Intent intent = new Intent(action);
intent.setComponent(serviceName);
return PendingIntent.getService(service, 0, intent, 0);
}
}

View file

@ -0,0 +1,41 @@
package com.dkanada.gramophone.service.playback;
import androidx.annotation.Nullable;
public interface Playback {
boolean setDataSource(String path);
void setNextDataSource(@Nullable String path);
void setCallbacks(PlaybackCallbacks callbacks);
boolean isInitialized();
boolean start();
void stop();
void release();
boolean pause();
boolean isPlaying();
int duration();
int position();
int seek(int whereto);
boolean setVolume(float vol);
boolean setAudioSessionId(int sessionId);
int getAudioSessionId();
interface PlaybackCallbacks {
void onTrackWentToNext();
void onTrackEnded();
}
}

View file

@ -0,0 +1,66 @@
package com.dkanada.gramophone.shortcuts;
import android.content.Context;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.IconCompat;
import android.util.TypedValue;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.util.ImageUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
@RequiresApi(Build.VERSION_CODES.N_MR1)
public final class AppShortcutIconGenerator {
public static Icon generateThemedIcon(Context context, int iconId) {
if (PreferenceUtil.getInstance(context).getColoredShortcuts()) {
return generateUserThemedIcon(context, iconId).toIcon();
} else {
return generateDefaultThemedIcon(context, iconId).toIcon();
}
}
private static IconCompat generateDefaultThemedIcon(Context context, int iconId) {
// Return an Icon of iconId with default colors
return generateThemedIcon(context, iconId,
context.getColor(R.color.app_shortcut_default_foreground),
context.getColor(R.color.app_shortcut_default_background)
);
}
private static IconCompat generateUserThemedIcon(Context context, int iconId) {
// Get background color from context's theme
final TypedValue typedColorBackground = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.colorBackground, typedColorBackground, true);
// Return an Icon of iconId with those colors
return generateThemedIcon(context, iconId,
ThemeStore.primaryColor(context),
typedColorBackground.data
);
}
private static IconCompat generateThemedIcon(Context context, int iconId, int foregroundColor, int backgroundColor) {
// Get and tint foreground and background drawables
Drawable vectorDrawable = ImageUtil.getTintedVectorDrawable(context, iconId, foregroundColor);
Drawable backgroundDrawable = ImageUtil.getTintedVectorDrawable(context, R.drawable.ic_app_shortcut_background, backgroundColor);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AdaptiveIconDrawable adaptiveIconDrawable = new AdaptiveIconDrawable(backgroundDrawable, vectorDrawable);
return IconCompat.createWithAdaptiveBitmap(ImageUtil.createBitmap(adaptiveIconDrawable));
} else {
// Squash the two drawables together
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{backgroundDrawable, vectorDrawable});
// Return as an Icon
return IconCompat.createWithBitmap(ImageUtil.createBitmap(layerDrawable));
}
}
}

View file

@ -0,0 +1,61 @@
package com.dkanada.gramophone.shortcuts;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import com.dkanada.gramophone.shortcuts.shortcuttype.LatestShortcutType;
import com.dkanada.gramophone.shortcuts.shortcuttype.ShuffleShortcutType;
import com.dkanada.gramophone.shortcuts.shortcuttype.FrequentShortcutType;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.service.MusicService;
public class AppShortcutLauncherActivity extends Activity {
public static final String KEY_SHORTCUT_TYPE = "com.kabouzeid.gramophone.shortcuts.ShortcutType";
public static final int SHORTCUT_TYPE_SHUFFLE = 0;
public static final int SHORTCUT_TYPE_FREQUENT = 1;
public static final int SHORTCUT_TYPE_LATEST = 2;
public static final int SHORTCUT_TYPE_NONE = 3;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int shortcutType = SHORTCUT_TYPE_NONE;
// Set shortcutType from the intent extras
Bundle extras = getIntent().getExtras();
if (extras != null) {
//noinspection WrongConstant
shortcutType = extras.getInt(KEY_SHORTCUT_TYPE, SHORTCUT_TYPE_NONE);
}
switch (shortcutType) {
case SHORTCUT_TYPE_SHUFFLE:
DynamicShortcutManager.reportShortcutUsed(this, ShuffleShortcutType.getId());
break;
case SHORTCUT_TYPE_FREQUENT:
DynamicShortcutManager.reportShortcutUsed(this, FrequentShortcutType.getId());
break;
case SHORTCUT_TYPE_LATEST:
DynamicShortcutManager.reportShortcutUsed(this, LatestShortcutType.getId());
break;
}
finish();
}
private void startServiceWithPlaylist(int shuffleMode, Playlist playlist) {
Intent intent = new Intent(this, MusicService.class);
intent.setAction(MusicService.ACTION_PLAY_PLAYLIST);
Bundle bundle = new Bundle();
bundle.putParcelable(MusicService.INTENT_EXTRA_PLAYLIST, playlist);
bundle.putInt(MusicService.INTENT_EXTRA_SHUFFLE_MODE, shuffleMode);
intent.putExtras(bundle);
startService(intent);
}
}

View file

@ -0,0 +1,59 @@
package com.dkanada.gramophone.shortcuts;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.dkanada.gramophone.shortcuts.shortcuttype.LatestShortcutType;
import com.dkanada.gramophone.shortcuts.shortcuttype.ShuffleShortcutType;
import com.dkanada.gramophone.shortcuts.shortcuttype.FrequentShortcutType;
import java.util.Arrays;
import java.util.List;
@TargetApi(Build.VERSION_CODES.N_MR1)
public class DynamicShortcutManager {
private Context context;
private ShortcutManager shortcutManager;
public DynamicShortcutManager(Context context) {
this.context = context;
shortcutManager = this.context.getSystemService(ShortcutManager.class);
}
public static ShortcutInfo createShortcut(Context context, String id, String shortLabel, String longLabel, Icon icon, Intent intent) {
return new ShortcutInfo.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(icon)
.setIntent(intent)
.build();
}
public void initDynamicShortcuts() {
if (shortcutManager.getDynamicShortcuts().size() == 0) {
shortcutManager.setDynamicShortcuts(getDefaultShortcuts());
}
}
public void updateDynamicShortcuts() {
shortcutManager.updateShortcuts(getDefaultShortcuts());
}
public List<ShortcutInfo> getDefaultShortcuts() {
return (Arrays.asList(
new ShuffleShortcutType(context).getShortcutInfo(),
new FrequentShortcutType(context).getShortcutInfo(),
new LatestShortcutType(context).getShortcutInfo()
));
}
public static void reportShortcutUsed(Context context, String shortcutId){
context.getSystemService(ShortcutManager.class).reportShortcutUsed(shortcutId);
}
}

View file

@ -0,0 +1,46 @@
package com.dkanada.gramophone.shortcuts.shortcuttype;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.os.Build;
import android.os.Bundle;
import com.dkanada.gramophone.shortcuts.AppShortcutLauncherActivity;
@TargetApi(Build.VERSION_CODES.N_MR1)
public abstract class BaseShortcutType {
static final String ID_PREFIX = "com.kabouzeid.gramophone.shortcuts.id.";
Context context;
public BaseShortcutType(Context context) {
this.context = context;
}
static public String getId() {
return ID_PREFIX + "invalid";
}
abstract ShortcutInfo getShortcutInfo();
/**
* Creates an Intent that will launch MainActivtiy and immediately play {@param songs} in either shuffle or normal mode
*
* @param shortcutType Describes the type of shortcut to create (ShuffleAll, TopTracks, custom playlist, etc.)
* @return
*/
Intent getPlaySongsIntent(int shortcutType) {
Intent intent = new Intent(context, AppShortcutLauncherActivity.class);
intent.setAction(Intent.ACTION_VIEW);
Bundle b = new Bundle();
b.putInt(AppShortcutLauncherActivity.KEY_SHORTCUT_TYPE, shortcutType);
intent.putExtras(b);
return intent;
}
}

View file

@ -0,0 +1,30 @@
package com.dkanada.gramophone.shortcuts.shortcuttype;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.Build;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.shortcuts.AppShortcutIconGenerator;
import com.dkanada.gramophone.shortcuts.AppShortcutLauncherActivity;
@TargetApi(Build.VERSION_CODES.N_MR1)
public final class FrequentShortcutType extends BaseShortcutType {
public FrequentShortcutType(Context context) {
super(context);
}
public static String getId() {
return ID_PREFIX + "top_tracks";
}
public ShortcutInfo getShortcutInfo() {
return new ShortcutInfo.Builder(context, getId())
.setShortLabel(context.getString(R.string.app_shortcut_top_tracks_short))
.setLongLabel(context.getString(R.string.my_top_tracks))
.setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_top_tracks))
.setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_FREQUENT))
.build();
}
}

View file

@ -0,0 +1,30 @@
package com.dkanada.gramophone.shortcuts.shortcuttype;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.Build;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.shortcuts.AppShortcutIconGenerator;
import com.dkanada.gramophone.shortcuts.AppShortcutLauncherActivity;
@TargetApi(Build.VERSION_CODES.N_MR1)
public final class LatestShortcutType extends BaseShortcutType {
public LatestShortcutType(Context context) {
super(context);
}
public static String getId() {
return ID_PREFIX + "last_added";
}
public ShortcutInfo getShortcutInfo() {
return new ShortcutInfo.Builder(context, getId())
.setShortLabel(context.getString(R.string.app_shortcut_last_added_short))
.setLongLabel(context.getString(R.string.last_added))
.setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_last_added))
.setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_LATEST))
.build();
}
}

View file

@ -0,0 +1,30 @@
package com.dkanada.gramophone.shortcuts.shortcuttype;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.Build;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.shortcuts.AppShortcutIconGenerator;
import com.dkanada.gramophone.shortcuts.AppShortcutLauncherActivity;
@TargetApi(Build.VERSION_CODES.N_MR1)
public final class ShuffleShortcutType extends BaseShortcutType {
public ShuffleShortcutType(Context context) {
super(context);
}
public static String getId() {
return ID_PREFIX + "shuffle_all";
}
public ShortcutInfo getShortcutInfo() {
return new ShortcutInfo.Builder(context, getId())
.setShortLabel(context.getString(R.string.app_shortcut_shuffle_all_short))
.setLongLabel(context.getString(R.string.action_shuffle_all))
.setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_shuffle_all))
.setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_SHUFFLE))
.build();
}
}

View file

@ -0,0 +1,198 @@
package com.dkanada.gramophone.ui.activities;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.ui.activities.base.AbsBaseActivity;
import butterknife.BindView;
import butterknife.ButterKnife;
@SuppressWarnings("FieldCanBeLocal")
public class AboutActivity extends AbsBaseActivity implements View.OnClickListener {
private static String GITHUB = "https://github.com/dkanada/gelli";
private static String TWITTER = "https://twitter.com/karimjabouzeid";
private static String WEBSITE = "https://github.com/dkanada";
private static String TRANSLATE = "https://phonograph.oneskyapp.com/collaboration/project?id=26521";
private static String RATE_ON_GOOGLE_PLAY = "https://play.google.com/store/apps/details?id=com.kabouzeid.gramophone";
private static String AIDAN_FOLLESTAD_GOOGLE_PLUS = "https://google.com/+AidanFollestad";
private static String AIDAN_FOLLESTAD_GITHUB = "https://github.com/afollestad";
private static String MAARTEN_CORPEL_GOOGLE_PLUS = "https://google.com/+MaartenCorpel";
private static String ALEKSANDAR_TESIC_GOOGLE_PLUS = "https://google.com/+aleksandartešić";
private static String EUGENE_CHEUNG_GITHUB = "https://github.com/arkon";
private static String EUGENE_CHEUNG_WEBSITE = "https://echeung.me/";
private static String ADRIAN_TWITTER = "https://twitter.com/froschgames";
private static String ADRIAN_WEBSITE = "https://froschgames.com/";
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.app_version)
TextView appVersion;
@BindView(R.id.app_source)
LinearLayout appSource;
@BindView(R.id.write_an_email)
LinearLayout writeAnEmail;
@BindView(R.id.follow_on_twitter)
LinearLayout followOnTwitter;
@BindView(R.id.visit_website)
LinearLayout visitWebsite;
@BindView(R.id.report_bugs)
LinearLayout reportBugs;
@BindView(R.id.translate)
LinearLayout translate;
@BindView(R.id.donate)
LinearLayout donate;
@BindView(R.id.rate_on_google_play)
LinearLayout rateOnGooglePlay;
@BindView(R.id.aidan_follestad_google_plus)
AppCompatButton aidanFollestadGooglePlus;
@BindView(R.id.aidan_follestad_git_hub)
AppCompatButton aidanFollestadGitHub;
@BindView(R.id.maarten_corpel_google_plus)
AppCompatButton maartenCorpelGooglePlus;
@BindView(R.id.aleksandar_tesic_google_plus)
AppCompatButton aleksandarTesicGooglePlus;
@BindView(R.id.eugene_cheung_git_hub)
AppCompatButton eugeneCheungGitHub;
@BindView(R.id.eugene_cheung_website)
AppCompatButton eugeneCheungWebsite;
@BindView(R.id.adrian_twitter)
AppCompatButton adrianTwitter;
@BindView(R.id.adrian_website)
AppCompatButton adrianWebsite;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
setUpViews();
}
private void setUpViews() {
setUpToolbar();
setUpAppVersion();
setUpOnClickListeners();
}
private void setUpToolbar() {
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void setUpAppVersion() {
appVersion.setText(getCurrentVersionName(this));
}
private void setUpOnClickListeners() {
followOnTwitter.setOnClickListener(this);
appSource.setOnClickListener(this);
visitWebsite.setOnClickListener(this);
reportBugs.setOnClickListener(this);
writeAnEmail.setOnClickListener(this);
translate.setOnClickListener(this);
rateOnGooglePlay.setOnClickListener(this);
donate.setOnClickListener(this);
aidanFollestadGooglePlus.setOnClickListener(this);
aidanFollestadGitHub.setOnClickListener(this);
maartenCorpelGooglePlus.setOnClickListener(this);
aleksandarTesicGooglePlus.setOnClickListener(this);
eugeneCheungGitHub.setOnClickListener(this);
eugeneCheungWebsite.setOnClickListener(this);
adrianTwitter.setOnClickListener(this);
adrianWebsite.setOnClickListener(this);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private static String getCurrentVersionName(@NonNull final Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "Unknown";
}
@Override
public void onClick(View v) {
if (v == followOnTwitter) {
openUrl(TWITTER);
} else if (v == appSource) {
openUrl(GITHUB);
} else if (v == visitWebsite) {
openUrl(WEBSITE);
} else if (v == reportBugs) {
openUrl(GITHUB);
} else if (v == writeAnEmail) {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:contact@kabouzeid.com"));
intent.putExtra(Intent.EXTRA_EMAIL, "contact@kabouzeid.com");
intent.putExtra(Intent.EXTRA_SUBJECT, "Phonograph");
startActivity(Intent.createChooser(intent, "E-Mail"));
} else if (v == translate) {
openUrl(TRANSLATE);
} else if (v == rateOnGooglePlay) {
openUrl(RATE_ON_GOOGLE_PLAY);
} else if (v == donate) {
openUrl(RATE_ON_GOOGLE_PLAY);
} else if (v == aidanFollestadGooglePlus) {
openUrl(AIDAN_FOLLESTAD_GOOGLE_PLUS);
} else if (v == aidanFollestadGitHub) {
openUrl(AIDAN_FOLLESTAD_GITHUB);
} else if (v == maartenCorpelGooglePlus) {
openUrl(MAARTEN_CORPEL_GOOGLE_PLUS);
} else if (v == aleksandarTesicGooglePlus) {
openUrl(ALEKSANDAR_TESIC_GOOGLE_PLUS);
} else if (v == eugeneCheungGitHub) {
openUrl(EUGENE_CHEUNG_GITHUB);
} else if (v == eugeneCheungWebsite) {
openUrl(EUGENE_CHEUNG_WEBSITE);
} else if (v == adrianTwitter) {
openUrl(ADRIAN_TWITTER);
} else if (v == adrianWebsite) {
openUrl(ADRIAN_WEBSITE);
}
}
private void openUrl(String url) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
}

View file

@ -0,0 +1,344 @@
package com.dkanada.gramophone.ui.activities;
import android.graphics.PorterDuff;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.afollestad.materialcab.MaterialCab;
import com.afollestad.materialdialogs.util.DialogUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.song.AlbumSongAdapter;
import com.dkanada.gramophone.dialogs.AddToPlaylistDialog;
import com.dkanada.gramophone.dialogs.SleepTimerDialog;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.interfaces.PaletteColorHolder;
import com.dkanada.gramophone.misc.SimpleObservableScrollViewCallbacks;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.NavigationUtil;
import com.dkanada.gramophone.util.ThemeUtil;
import com.dkanada.gramophone.util.QueryUtil;
import org.jellyfin.apiclient.model.querying.ItemQuery;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class AlbumDetailActivity extends AbsSlidingMusicPanelActivity implements PaletteColorHolder, CabHolder {
public static final String EXTRA_ALBUM = "extra_album";
private Album album;
@BindView(R.id.list)
ObservableRecyclerView recyclerView;
@BindView(R.id.image)
ImageView albumArtImageView;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.header)
View headerView;
@BindView(R.id.header_overlay)
View headerOverlay;
@BindView(R.id.artist_icon)
ImageView artistIconImageView;
@BindView(R.id.duration_icon)
ImageView durationIconImageView;
@BindView(R.id.song_count_icon)
ImageView songCountIconImageView;
@BindView(R.id.album_year_icon)
ImageView albumYearIconImageView;
@BindView(R.id.artist_text)
TextView artistTextView;
@BindView(R.id.duration_text)
TextView durationTextView;
@BindView(R.id.song_count_text)
TextView songCountTextView;
@BindView(R.id.album_year_text)
TextView albumYearTextView;
private AlbumSongAdapter adapter;
private MaterialCab cab;
private int headerViewHeight;
private int toolbarColor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setUpObservableListViewParams();
setUpToolBar();
setUpViews();
if (Build.VERSION.SDK_INT > 21) postponeEnterTransition();
Album album = getIntent().getExtras().getParcelable(EXTRA_ALBUM);
loadAlbumCover(album.primary);
setAlbum(album);
ItemQuery query = new ItemQuery();
query.setParentId(album.id);
query.setSortBy(new String[]{"IndexNumber"});
QueryUtil.getSongs(query, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
album.songs = (List<Song>) media;
setAlbum(album);
}
});
}
@Override
protected View createContentView() {
return wrapSlidingMusicPanel(R.layout.activity_album_detail);
}
private final SimpleObservableScrollViewCallbacks observableScrollViewCallbacks = new SimpleObservableScrollViewCallbacks() {
@Override
public void onScrollChanged(int scrollY, boolean b, boolean b2) {
scrollY += headerViewHeight;
// Change alpha of overlay
float headerAlpha = Math.max(0, Math.min(1, (float) 2 * scrollY / headerViewHeight));
headerOverlay.setBackgroundColor(ColorUtil.withAlpha(toolbarColor, headerAlpha));
// Translate name text
headerView.setTranslationY(Math.max(-scrollY, -headerViewHeight));
headerOverlay.setTranslationY(Math.max(-scrollY, -headerViewHeight));
albumArtImageView.setTranslationY(Math.max(-scrollY, -headerViewHeight));
}
};
private void setUpObservableListViewParams() {
headerViewHeight = getResources().getDimensionPixelSize(R.dimen.detail_header_height);
}
private void setUpViews() {
setUpRecyclerView();
setUpSongsAdapter();
artistTextView.setOnClickListener(v -> {
if (album != null) {
NavigationUtil.goToArtist(AlbumDetailActivity.this, new Artist(album));
}
});
setColors(DialogUtils.resolveColor(this, R.attr.defaultFooterColor));
}
private void loadAlbumCover(String primary) {
CustomGlideRequest.Builder
.from(Glide.with(this), primary)
.generatePalette(this).build()
.listener(new RequestListener<Object, BitmapPaletteWrapper>() {
@Override
public boolean onException(Exception e, Object model, Target<BitmapPaletteWrapper> target, boolean isFirstResource) {
if (Build.VERSION.SDK_INT > 21) startPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(BitmapPaletteWrapper resource, Object model, Target<BitmapPaletteWrapper> target, boolean dataSource, boolean isFirstResource) {
if (Build.VERSION.SDK_INT > 21) startPostponedEnterTransition();
return false;
}
})
.dontAnimate()
.into(new CustomPaletteTarget(albumArtImageView) {
@Override
public void onColorReady(int color) {
setColors(color);
}
});
}
private void setColors(int color) {
toolbarColor = color;
headerView.setBackgroundColor(color);
setNavigationbarColor(color);
setTaskDescriptionColor(color);
toolbar.setBackgroundColor(color);
// needed to auto readjust the toolbar content color
setSupportActionBar(toolbar);
setStatusbarColor(color);
int secondaryTextColor = MaterialValueHelper.getSecondaryTextColor(this, ColorUtil.isColorLight(color));
artistIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
durationIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
songCountIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
albumYearIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
artistTextView.setTextColor(MaterialValueHelper.getPrimaryTextColor(this, ColorUtil.isColorLight(color)));
durationTextView.setTextColor(secondaryTextColor);
songCountTextView.setTextColor(secondaryTextColor);
albumYearTextView.setTextColor(secondaryTextColor);
}
@Override
public int getPaletteColor() {
return toolbarColor;
}
private void setUpRecyclerView() {
setUpRecyclerViewPadding();
recyclerView.setScrollViewCallbacks(observableScrollViewCallbacks);
final View contentView = getWindow().getDecorView().findViewById(android.R.id.content);
contentView.post(() -> observableScrollViewCallbacks.onScrollChanged(-headerViewHeight, false, false));
}
private void setUpRecyclerViewPadding() {
recyclerView.setPadding(0, headerViewHeight, 0, 0);
}
private void setUpToolBar() {
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setTitle(null);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void setUpSongsAdapter() {
adapter = new AlbumSongAdapter(this, getAlbum().songs, R.layout.item_list, false, this);
recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
recyclerView.setAdapter(adapter);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
if (adapter.getItemCount() == 0) finish();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_album_detail, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
final List<Song> songs = adapter.getDataSet();
switch (id) {
case R.id.action_sleep_timer:
new SleepTimerDialog().show(getSupportFragmentManager(), "SET_SLEEP_TIMER");
return true;
case R.id.action_shuffle_album:
MusicPlayerRemote.openAndShuffleQueue(songs, true);
return true;
case R.id.action_play_next:
MusicPlayerRemote.playNext(songs);
return true;
case R.id.action_add_to_queue:
MusicPlayerRemote.enqueue(songs);
return true;
case R.id.action_add_to_playlist:
AddToPlaylistDialog.create(songs).show(getSupportFragmentManager(), "ADD_PLAYLIST");
return true;
case android.R.id.home:
super.onBackPressed();
return true;
case R.id.action_go_to_artist:
NavigationUtil.goToArtist(this, new Artist(album));
return true;
}
return super.onOptionsItemSelected(item);
}
@NonNull
@Override
public MaterialCab openCab(int menuRes, @NonNull final MaterialCab.Callback callback) {
if (cab != null && cab.isActive()) cab.finish();
cab = new MaterialCab(this, R.id.cab_stub)
.setMenu(menuRes)
.setCloseDrawableRes(R.drawable.ic_close_white_24dp)
.setBackgroundColor(ThemeUtil.shiftBackgroundColorForLightText(getPaletteColor()))
.start(new MaterialCab.Callback() {
@Override
public boolean onCabCreated(MaterialCab materialCab, Menu menu) {
return callback.onCabCreated(materialCab, menu);
}
@Override
public boolean onCabItemClicked(MenuItem menuItem) {
return callback.onCabItemClicked(menuItem);
}
@Override
public boolean onCabFinished(MaterialCab materialCab) {
return callback.onCabFinished(materialCab);
}
});
return cab;
}
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) {
cab.finish();
} else {
recyclerView.stopScroll();
super.onBackPressed();
}
}
@Override
public void onMediaStoreChanged() {
super.onMediaStoreChanged();
}
@Override
public void setStatusbarColor(int color) {
super.setStatusbarColor(color);
setLightStatusbar(false);
}
private void setAlbum(Album album) {
this.album = album;
getSupportActionBar().setTitle(album.getTitle());
artistTextView.setText(album.getArtistName());
songCountTextView.setText(MusicUtil.getSongCountString(this, album.getSongCount()));
durationTextView.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, album.songs)));
albumYearTextView.setText(MusicUtil.getYearString(album.getYear()));
if (album.songs.size() != 0) adapter.swapDataSet(album.songs);
}
private Album getAlbum() {
if (album == null) album = new Album();
return album;
}
}

View file

@ -0,0 +1,368 @@
package com.dkanada.gramophone.ui.activities;
import android.graphics.PorterDuff;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import java.util.List;
import com.afollestad.materialcab.MaterialCab;
import com.afollestad.materialdialogs.util.DialogUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.ksoichiro.android.observablescrollview.ObservableListView;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.album.HorizontalAlbumAdapter;
import com.dkanada.gramophone.adapter.song.ArtistSongAdapter;
import com.dkanada.gramophone.dialogs.AddToPlaylistDialog;
import com.dkanada.gramophone.dialogs.SleepTimerDialog;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.glide.CustomPaletteTarget;
import com.dkanada.gramophone.glide.palette.BitmapPaletteWrapper;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.interfaces.PaletteColorHolder;
import com.dkanada.gramophone.misc.SimpleObservableScrollViewCallbacks;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.ThemeUtil;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.dkanada.gramophone.util.QueryUtil;
import org.jellyfin.apiclient.model.querying.ItemQuery;
public class ArtistDetailActivity extends AbsSlidingMusicPanelActivity implements PaletteColorHolder, CabHolder {
public static final String EXTRA_ARTIST = "extra_artist";
@BindView(R.id.list)
ObservableListView songListView;
@BindView(R.id.image)
ImageView artistImage;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.header)
View headerView;
@BindView(R.id.header_overlay)
View headerOverlay;
@BindView(R.id.duration_icon)
ImageView durationIconImageView;
@BindView(R.id.song_count_icon)
ImageView songCountIconImageView;
@BindView(R.id.album_count_icon)
ImageView albumCountIconImageView;
@BindView(R.id.duration_text)
TextView durationTextView;
@BindView(R.id.song_count_text)
TextView songCountTextView;
@BindView(R.id.album_count_text)
TextView albumCountTextView;
View songListHeader;
RecyclerView albumRecyclerView;
private MaterialCab cab;
private int headerViewHeight;
private int toolbarColor;
private Artist artist;
private HorizontalAlbumAdapter albumAdapter;
private ArtistSongAdapter songAdapter;
private final SimpleObservableScrollViewCallbacks observableScrollViewCallbacks = new SimpleObservableScrollViewCallbacks() {
@Override
public void onScrollChanged(int scrollY, boolean b, boolean b2) {
scrollY += headerViewHeight;
// Change alpha of overlay
float headerAlpha = Math.max(0, Math.min(1, (float) 2 * scrollY / headerViewHeight));
headerOverlay.setBackgroundColor(ColorUtil.withAlpha(toolbarColor, headerAlpha));
// Translate name text
headerView.setTranslationY(Math.max(-scrollY, -headerViewHeight));
headerOverlay.setTranslationY(Math.max(-scrollY, -headerViewHeight));
artistImage.setTranslationY(Math.max(-scrollY, -headerViewHeight));
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDrawUnderStatusbar();
ButterKnife.bind(this);
usePalette = PreferenceUtil.getInstance(this).getAlbumArtistColoredFooters();
initViews();
setUpObservableListViewParams();
setUpToolbar();
setUpViews();
if (Build.VERSION.SDK_INT > 21) postponeEnterTransition();
Artist artist = getIntent().getExtras().getParcelable(EXTRA_ARTIST);
loadArtistImage(artist.primary);
setArtist(artist);
ItemQuery albums = new ItemQuery();
albums.setArtistIds(new String[]{artist.id});
QueryUtil.getAlbums(albums, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
artist.albums = (List<Album>) media;
setArtist(artist);
}
});
ItemQuery songs = new ItemQuery();
songs.setArtistIds(new String[]{artist.id});
QueryUtil.getSongs(songs, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
artist.songs = (List<Song>) media;
setArtist(artist);
}
});
}
@Override
protected View createContentView() {
return wrapSlidingMusicPanel(R.layout.activity_artist_detail);
}
private boolean usePalette;
private void setUpObservableListViewParams() {
headerViewHeight = getResources().getDimensionPixelSize(R.dimen.detail_header_height);
}
private void initViews() {
songListHeader = LayoutInflater.from(this).inflate(R.layout.artist_detail_header, songListView, false);
albumRecyclerView = songListHeader.findViewById(R.id.recycler_view);
}
private void setUpViews() {
setUpSongListView();
setUpAlbumRecyclerView();
setColors(DialogUtils.resolveColor(this, R.attr.defaultFooterColor));
}
private void setUpSongListView() {
setUpSongListPadding();
songListView.setScrollViewCallbacks(observableScrollViewCallbacks);
songListView.addHeaderView(songListHeader);
songAdapter = new ArtistSongAdapter(this, getArtist().getSongs(), this);
songListView.setAdapter(songAdapter);
final View contentView = getWindow().getDecorView().findViewById(android.R.id.content);
contentView.post(() -> observableScrollViewCallbacks.onScrollChanged(-headerViewHeight, false, false));
}
private void setUpSongListPadding() {
songListView.setPadding(0, headerViewHeight, 0, 0);
}
private void setUpAlbumRecyclerView() {
albumRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
albumAdapter = new HorizontalAlbumAdapter(this, getArtist().albums, usePalette, this);
albumRecyclerView.setAdapter(albumAdapter);
albumAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
if (albumAdapter.getItemCount() == 0) finish();
}
});
}
protected void setUsePalette(boolean usePalette) {
albumAdapter.usePalette(usePalette);
PreferenceUtil.getInstance(this).setAlbumArtistColoredFooters(usePalette);
this.usePalette = usePalette;
}
private void loadArtistImage(String primary) {
CustomGlideRequest.Builder
.from(Glide.with(this), primary)
.generatePalette(this).build()
.listener(new RequestListener<Object, BitmapPaletteWrapper>() {
@Override
public boolean onException(Exception e, Object model, Target<BitmapPaletteWrapper> target, boolean isFirstResource) {
if (Build.VERSION.SDK_INT > 21) startPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(BitmapPaletteWrapper resource, Object model, Target<BitmapPaletteWrapper> target, boolean dataSource, boolean isFirstResource) {
if (Build.VERSION.SDK_INT > 21) startPostponedEnterTransition();
return false;
}
})
.dontAnimate()
.into(new CustomPaletteTarget(artistImage) {
@Override
public void onColorReady(int color) {
setColors(color);
}
});
}
@Override
public int getPaletteColor() {
return toolbarColor;
}
private void setColors(int color) {
toolbarColor = color;
headerView.setBackgroundColor(color);
setNavigationbarColor(color);
setTaskDescriptionColor(color);
toolbar.setBackgroundColor(color);
// needed to auto readjust the toolbar content color
setSupportActionBar(toolbar);
setStatusbarColor(color);
int secondaryTextColor = MaterialValueHelper.getSecondaryTextColor(this, ColorUtil.isColorLight(color));
durationIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
songCountIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
albumCountIconImageView.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN);
durationTextView.setTextColor(secondaryTextColor);
songCountTextView.setTextColor(secondaryTextColor);
albumCountTextView.setTextColor(secondaryTextColor);
}
private void setUpToolbar() {
setSupportActionBar(toolbar);
// noinspection ConstantConditions
getSupportActionBar().setTitle(null);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_artist_detail, menu);
menu.findItem(R.id.action_colored_footers).setChecked(usePalette);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
final List<Song> songs = songAdapter.getDataSet();
switch (id) {
case R.id.action_sleep_timer:
new SleepTimerDialog().show(getSupportFragmentManager(), "SET_SLEEP_TIMER");
return true;
case R.id.action_shuffle_artist:
MusicPlayerRemote.openAndShuffleQueue(songs, true);
return true;
case R.id.action_play_next:
MusicPlayerRemote.playNext(songs);
return true;
case R.id.action_add_to_queue:
MusicPlayerRemote.enqueue(songs);
return true;
case R.id.action_add_to_playlist:
AddToPlaylistDialog.create(songs).show(getSupportFragmentManager(), "ADD_PLAYLIST");
return true;
case android.R.id.home:
super.onBackPressed();
return true;
case R.id.action_colored_footers:
item.setChecked(!item.isChecked());
setUsePalette(item.isChecked());
return true;
}
return super.onOptionsItemSelected(item);
}
@NonNull
@Override
public MaterialCab openCab(int menuRes, @NonNull final MaterialCab.Callback callback) {
if (cab != null && cab.isActive()) cab.finish();
cab = new MaterialCab(this, R.id.cab_stub)
.setMenu(menuRes)
.setCloseDrawableRes(R.drawable.ic_close_white_24dp)
.setBackgroundColor(ThemeUtil.shiftBackgroundColorForLightText(getPaletteColor()))
.start(new MaterialCab.Callback() {
@Override
public boolean onCabCreated(MaterialCab materialCab, Menu menu) {
return callback.onCabCreated(materialCab, menu);
}
@Override
public boolean onCabItemClicked(MenuItem menuItem) {
return callback.onCabItemClicked(menuItem);
}
@Override
public boolean onCabFinished(MaterialCab materialCab) {
return callback.onCabFinished(materialCab);
}
});
return cab;
}
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) {
cab.finish();
} else {
albumRecyclerView.stopScroll();
super.onBackPressed();
}
}
@Override
public void onMediaStoreChanged() {
super.onMediaStoreChanged();
}
@Override
public void setStatusbarColor(int color) {
super.setStatusbarColor(color);
setLightStatusbar(false);
}
private void setArtist(Artist artist) {
this.artist = artist;
getSupportActionBar().setTitle(artist.getName());
songCountTextView.setText(MusicUtil.getSongCountString(this, artist.getSongCount()));
albumCountTextView.setText(MusicUtil.getAlbumCountString(this, artist.getAlbumCount()));
durationTextView.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, artist.getSongs())));
// TODO this activity will crash when an artist is passed with an empty album array
// something in the album adapter is causing the issue
if (artist.albums.size() != 0) songAdapter.swapDataSet(artist.songs);
if (artist.albums.size() != 0) albumAdapter.swapDataSet(artist.albums);
}
private Artist getArtist() {
if (artist == null) artist = new Artist();
return artist;
}
}

View file

@ -0,0 +1,176 @@
package com.dkanada.gramophone.ui.activities;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.afollestad.materialcab.MaterialCab;
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.song.SongAdapter;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.model.Genre;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.dkanada.gramophone.util.ThemeUtil;
import com.dkanada.gramophone.util.QueryUtil;
import com.dkanada.gramophone.util.ViewUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import org.jellyfin.apiclient.model.querying.ItemQuery;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class GenreDetailActivity extends AbsSlidingMusicPanelActivity implements CabHolder {
public static final String EXTRA_GENRE = "extra_genre";
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(android.R.id.empty)
TextView empty;
private Genre genre;
private MaterialCab cab;
private SongAdapter adapter;
private RecyclerView.Adapter wrappedAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
genre = getIntent().getExtras().getParcelable(EXTRA_GENRE);
setUpRecyclerView();
setUpToolBar();
ItemQuery query = new ItemQuery();
query.setGenreIds(new String[]{genre.id});
QueryUtil.getSongs(query, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
adapter.getDataSet().addAll((List<Song>) media);
adapter.notifyDataSetChanged();
}
});
}
@Override
protected View createContentView() {
return wrapSlidingMusicPanel(R.layout.activity_genre_detail);
}
private void setUpRecyclerView() {
ViewUtil.setUpFastScrollRecyclerViewColor(this, ((FastScrollRecyclerView) recyclerView), ThemeStore.accentColor(this));
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new SongAdapter(this, new ArrayList<>(), R.layout.item_list, false, this);
recyclerView.setAdapter(adapter);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
checkIsEmpty();
}
});
}
private void setUpToolBar() {
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setTitle(genre.name);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_genre_detail, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_shuffle_genre:
MusicPlayerRemote.openAndShuffleQueue(adapter.getDataSet(), true);
return true;
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@NonNull
@Override
public MaterialCab openCab(final int menu, final MaterialCab.Callback callback) {
if (cab != null && cab.isActive()) cab.finish();
cab = new MaterialCab(this, R.id.cab_stub)
.setMenu(menu)
.setCloseDrawableRes(R.drawable.ic_close_white_24dp)
.setBackgroundColor(ThemeUtil.shiftBackgroundColorForLightText(ThemeStore.primaryColor(this)))
.start(callback);
return cab;
}
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) cab.finish();
else {
recyclerView.stopScroll();
super.onBackPressed();
}
}
@Override
public void onMediaStoreChanged() {
super.onMediaStoreChanged();
}
private void checkIsEmpty() {
empty.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
@Override
protected void onDestroy() {
if (recyclerView != null) {
recyclerView.setAdapter(null);
recyclerView = null;
}
if (wrappedAdapter != null) {
WrapperAdapterUtils.releaseAll(wrappedAdapter);
wrappedAdapter = null;
}
adapter = null;
super.onDestroy();
}
}

View file

@ -0,0 +1,119 @@
package com.dkanada.gramophone.ui.activities;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.App;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.ui.activities.base.AbsBaseActivity;
import org.jellyfin.apiclient.interaction.AndroidCredentialProvider;
import org.jellyfin.apiclient.interaction.ConnectionResult;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
import org.jellyfin.apiclient.interaction.connectionmanager.ConnectionManager;
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.model.apiclient.ServerCredentials;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
import org.jellyfin.apiclient.model.serialization.IJsonSerializer;
import org.jellyfin.apiclient.model.users.AuthenticationResult;
import butterknife.BindView;
import butterknife.ButterKnife;
public class LoginActivity extends AbsBaseActivity implements View.OnClickListener {
public static final String TAG = SplashActivity.class.getSimpleName();
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.username)
EditText username;
@BindView(R.id.password)
EditText password;
@BindView(R.id.server)
EditText server;
@BindView(R.id.login)
Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
setUpViews();
}
private void setUpViews() {
setUpToolbar();
setUpOnClickListeners();
}
private void setUpToolbar() {
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void setUpOnClickListeners() {
login.setOnClickListener(this);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onClick(View v) {
if (v == login) {
final Context context = this;
IJsonSerializer jsonSerializer = new GsonJsonSerializer();
ILogger logger = new AndroidLogger(TAG);
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, this);
AndroidCredentialProvider credentialProvider = new AndroidCredentialProvider(jsonSerializer, this, logger);
ConnectionManager connectionManager = App.getConnectionManager(context, jsonSerializer, logger, httpClient);
connectionManager.Connect(server.getText().toString(), new Response<ConnectionResult>() {
@Override
public void onResponse(ConnectionResult result) {
App.setApiClient(result.getApiClient());
ServerCredentials serverCredentials = new ServerCredentials();
serverCredentials.AddOrUpdateServer(result.getServers().get(0));
App.getApiClient().AuthenticateUserAsync(username.getText().toString(), password.getText().toString(), new Response<AuthenticationResult>() {
@Override
public void onResponse(AuthenticationResult result) {
if (result.getAccessToken() == null) return;
serverCredentials.GetServer(result.getServerId()).setAccessToken(result.getAccessToken());
credentialProvider.SaveCredentials(serverCredentials);
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
}
});
}
});
}
}
}

View file

@ -0,0 +1,268 @@
package com.dkanada.gramophone.ui.activities;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.navigation.NavigationView;
import androidx.fragment.app.Fragment;
import androidx.drawerlayout.widget.DrawerLayout;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.kabouzeid.appthemehelper.util.ATHUtil;
import com.kabouzeid.appthemehelper.util.NavigationViewUtil;
import com.dkanada.gramophone.App;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.dkanada.gramophone.ui.fragments.mainactivity.library.LibraryFragment;
import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.QueryUtil;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
import org.jellyfin.apiclient.model.serialization.IJsonSerializer;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AbsSlidingMusicPanelActivity {
public static final String TAG = MainActivity.class.getSimpleName();
@BindView(R.id.navigation_view)
NavigationView navigationView;
@BindView(R.id.drawer_layout)
DrawerLayout drawerLayout;
@Nullable
MainActivityFragmentCallbacks currentFragment;
@Nullable
private View navigationDrawerHeader;
@Nullable
private List<BaseItemDto> libraries;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDrawUnderStatusbar();
ButterKnife.bind(this);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
navigationView.setFitsSystemWindows(false);
}
Menu menu = navigationView.getMenu();
QueryUtil.getLibraries(new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
libraries = (List<BaseItemDto>) media;
menu.clear();
for (BaseItemDto itemDto : libraries) {
if (menu.size() == 0) {
QueryUtil.currentLibrary = itemDto;
}
if (itemDto.getCollectionType() == null || !itemDto.getCollectionType().equals("music")) continue;
menu.add(R.id.navigation_drawer_menu_category_sections, itemDto.getId().hashCode(), menu.size(), itemDto.getName());
menu.getItem(menu.size() - 1).setIcon(R.drawable.ic_album_white_24dp);
}
menu.add(R.id.navigation_drawer_menu_category_other, R.id.nav_settings, menu.size(), R.string.action_settings);
menu.getItem(menu.size() - 1).setIcon(R.drawable.ic_settings_white_24dp);
menu.add(R.id.navigation_drawer_menu_category_other, R.id.nav_about, menu.size(), R.string.action_about);
menu.getItem(menu.size() - 1).setIcon(R.drawable.ic_info_outline_white_24dp);
menu.add(R.id.navigation_drawer_menu_category_other, R.id.nav_logout, menu.size(), R.string.logout);
menu.getItem(menu.size() - 1).setIcon(R.drawable.ic_exit_to_app_white_48dp);
setUpDrawerLayout();
menu.getItem(0).setChecked(true);
if (savedInstanceState == null) {
setCurrentFragment(LibraryFragment.newInstance());
} else {
restoreCurrentFragment();
}
}
});
}
private void setCurrentFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, null).commit();
currentFragment = (MainActivityFragmentCallbacks) fragment;
}
private void restoreCurrentFragment() {
currentFragment = (MainActivityFragmentCallbacks) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
}
@Override
protected View createContentView() {
@SuppressLint("InflateParams")
View contentView = getLayoutInflater().inflate(R.layout.activity_main_drawer_layout, null);
ViewGroup drawerContent = contentView.findViewById(R.id.drawer_content_container);
drawerContent.addView(wrapSlidingMusicPanel(R.layout.activity_main_content));
return contentView;
}
private void setUpNavigationView() {
int accentColor = ThemeStore.accentColor(this);
NavigationViewUtil.setItemIconColors(navigationView, ATHUtil.resolveColor(this, R.attr.iconColor, ThemeStore.textColorSecondary(this)), accentColor);
NavigationViewUtil.setItemTextColors(navigationView, ThemeStore.textColorPrimary(this), accentColor);
navigationView.setNavigationItemSelectedListener(menuItem -> {
drawerLayout.closeDrawers();
switch (menuItem.getItemId()) {
case R.id.nav_settings:
new Handler().postDelayed(() -> startActivity(new Intent(MainActivity.this, SettingsActivity.class)), 200);
break;
case R.id.nav_about:
new Handler().postDelayed(() -> startActivity(new Intent(MainActivity.this, AboutActivity.class)), 200);
break;
case R.id.nav_logout:
IJsonSerializer jsonSerializer = new GsonJsonSerializer();
ILogger logger = new AndroidLogger(TAG);
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, this);
App.getConnectionManager(this, jsonSerializer, logger, httpClient).Logout(new EmptyResponse());
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
this.startActivity(intent);
}
// only run the following code when a new library has been selected
if (menuItem.getItemId() == QueryUtil.currentLibrary.getId().hashCode()) return true;
for (BaseItemDto itemDto : libraries) {
if (menuItem.getItemId() == itemDto.getId().hashCode()) {
QueryUtil.currentLibrary = itemDto;
setCurrentFragment(LibraryFragment.newInstance());
break;
}
}
// setCheckable must be applied to the items on creation
// it also applies a tacky background color for the checked item
// this is a hack to check the current item without that
if (menuItem.getItemId() == R.id.nav_settings || menuItem.getItemId() == R.id.nav_about) return true;
for (int i = 0; i < navigationView.getMenu().size(); i++) {
if (navigationView.getMenu().getItem(i) == menuItem) {
navigationView.getMenu().getItem(i).setChecked(true);
} else {
navigationView.getMenu().getItem(i).setChecked(false);
}
}
return true;
});
}
private void setUpDrawerLayout() {
setUpNavigationView();
}
private void updateNavigationDrawerHeader() {
if (!MusicPlayerRemote.getPlayingQueue().isEmpty()) {
Song song = MusicPlayerRemote.getCurrentSong();
if (navigationDrawerHeader == null) {
navigationDrawerHeader = navigationView.inflateHeaderView(R.layout.navigation_drawer_header);
navigationDrawerHeader.setOnClickListener(v -> {
drawerLayout.closeDrawers();
if (getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
expandPanel();
}
});
}
((TextView) navigationDrawerHeader.findViewById(R.id.title)).setText(song.title);
((TextView) navigationDrawerHeader.findViewById(R.id.text)).setText(MusicUtil.getSongInfoString(song));
CustomGlideRequest.Builder
.from(Glide.with(this), song.primary)
.build().into(((ImageView) navigationDrawerHeader.findViewById(R.id.image)));
} else {
if (navigationDrawerHeader != null) {
navigationView.removeHeaderView(navigationDrawerHeader);
navigationDrawerHeader = null;
}
}
}
@Override
public void onPlayingMetaChanged() {
super.onPlayingMetaChanged();
updateNavigationDrawerHeader();
}
@Override
public void onServiceConnected() {
super.onServiceConnected();
updateNavigationDrawerHeader();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
if (drawerLayout.isDrawerOpen(navigationView)) {
drawerLayout.closeDrawer(navigationView);
} else {
drawerLayout.openDrawer(navigationView);
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean handleBackPress() {
if (drawerLayout.isDrawerOpen(navigationView)) {
drawerLayout.closeDrawers();
return true;
}
return super.handleBackPress() || (currentFragment != null && currentFragment.handleBackPress());
}
@Override
public void onPanelExpanded(View view) {
super.onPanelExpanded(view);
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
@Override
public void onPanelCollapsed(View view) {
super.onPanelCollapsed(view);
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
public interface MainActivityFragmentCallbacks {
boolean handleBackPress();
}
}

View file

@ -0,0 +1,220 @@
package com.dkanada.gramophone.ui.activities;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.afollestad.materialcab.MaterialCab;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.RefactoredDefaultItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.song.OrderablePlaylistSongAdapter;
import com.dkanada.gramophone.adapter.song.PlaylistSongAdapter;
import com.dkanada.gramophone.adapter.song.SongAdapter;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.helper.menu.PlaylistMenuHelper;
import com.dkanada.gramophone.interfaces.CabHolder;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.model.PlaylistSong;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.dkanada.gramophone.util.ThemeUtil;
import com.dkanada.gramophone.util.PlaylistUtil;
import com.dkanada.gramophone.util.ViewUtil;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
import org.jellyfin.apiclient.model.playlists.PlaylistItemQuery;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class PlaylistDetailActivity extends AbsSlidingMusicPanelActivity implements CabHolder {
@NonNull
public static String EXTRA_PLAYLIST = "extra_playlist";
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(android.R.id.empty)
TextView empty;
private Playlist playlist;
private MaterialCab cab;
private SongAdapter adapter;
private RecyclerView.Adapter wrappedAdapter;
private RecyclerViewDragDropManager recyclerViewDragDropManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
playlist = getIntent().getExtras().getParcelable(EXTRA_PLAYLIST);
setUpRecyclerView();
setUpToolbar();
PlaylistItemQuery query = new PlaylistItemQuery();
query.setId(playlist.id);
PlaylistUtil.getPlaylist(query, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
adapter.getDataSet().addAll((List<PlaylistSong>) media);
adapter.notifyDataSetChanged();
}
});
}
@Override
protected View createContentView() {
return wrapSlidingMusicPanel(R.layout.activity_playlist_detail);
}
private void setUpRecyclerView() {
ViewUtil.setUpFastScrollRecyclerViewColor(this, ((FastScrollRecyclerView) recyclerView), ThemeStore.accentColor(this));
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerViewDragDropManager = new RecyclerViewDragDropManager();
final GeneralItemAnimator animator = new RefactoredDefaultItemAnimator();
adapter = new OrderablePlaylistSongAdapter(this, new ArrayList<>(), R.layout.item_list, false, this, (fromPosition, toPosition) -> {
PlaylistUtil.moveItem(playlist.id, (PlaylistSong) adapter.getDataSet().get(fromPosition), toPosition);
Song song = adapter.getDataSet().remove(fromPosition);
adapter.getDataSet().add(toPosition, song);
adapter.notifyItemMoved(fromPosition, toPosition);
});
wrappedAdapter = recyclerViewDragDropManager.createWrappedAdapter(adapter);
recyclerView.setAdapter(wrappedAdapter);
recyclerView.setItemAnimator(animator);
recyclerViewDragDropManager.attachRecyclerView(recyclerView);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
checkIsEmpty();
}
});
}
private void setUpToolbar() {
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setToolbarTitle(playlist.name);
}
private void setToolbarTitle(String title) {
//noinspection ConstantConditions
getSupportActionBar().setTitle(title);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_playlist_detail, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_shuffle_playlist:
MusicPlayerRemote.openAndShuffleQueue(adapter.getDataSet(), true);
return true;
case android.R.id.home:
onBackPressed();
return true;
}
return PlaylistMenuHelper.handleMenuClick(this, playlist, item);
}
@NonNull
@Override
public MaterialCab openCab(final int menu, final MaterialCab.Callback callback) {
if (cab != null && cab.isActive()) cab.finish();
cab = new MaterialCab(this, R.id.cab_stub)
.setMenu(menu)
.setCloseDrawableRes(R.drawable.ic_close_white_24dp)
.setBackgroundColor(ThemeUtil.shiftBackgroundColorForLightText(ThemeStore.primaryColor(this)))
.start(callback);
return cab;
}
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) {
cab.finish();
} else {
recyclerView.stopScroll();
super.onBackPressed();
}
}
@Override
public void onMediaStoreChanged() {
super.onMediaStoreChanged();
}
private void checkIsEmpty() {
empty.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
@Override
public void onPause() {
if (recyclerViewDragDropManager != null) {
recyclerViewDragDropManager.cancelDrag();
}
super.onPause();
}
@Override
protected void onDestroy() {
if (recyclerViewDragDropManager != null) {
recyclerViewDragDropManager.release();
recyclerViewDragDropManager = null;
}
if (recyclerView != null) {
recyclerView.setItemAnimator(null);
recyclerView.setAdapter(null);
recyclerView = null;
}
if (wrappedAdapter != null) {
WrapperAdapterUtils.releaseAll(wrappedAdapter);
wrappedAdapter = null;
}
adapter = null;
super.onDestroy();
}
}

View file

@ -0,0 +1,199 @@
package com.dkanada.gramophone.ui.activities;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.adapter.SearchAdapter;
import com.dkanada.gramophone.interfaces.MediaCallback;
import com.dkanada.gramophone.model.Album;
import com.dkanada.gramophone.model.Artist;
import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.ui.activities.base.AbsMusicServiceActivity;
import com.dkanada.gramophone.util.QueryUtil;
import com.dkanada.gramophone.util.Util;
import org.jellyfin.apiclient.model.querying.ItemQuery;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SearchActivity extends AbsMusicServiceActivity implements SearchView.OnQueryTextListener {
public static final String QUERY = "query";
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(android.R.id.empty)
TextView empty;
SearchView searchView;
private Handler handler;
private SearchAdapter adapter;
private String query;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new SearchAdapter(this, Collections.emptyList());
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
empty.setVisibility(adapter.getItemCount() < 1 ? View.VISIBLE : View.GONE);
}
});
recyclerView.setAdapter(adapter);
recyclerView.setOnTouchListener((v, event) -> {
hideSoftKeyboard();
return false;
});
setUpToolBar();
handler = new Handler();
if (savedInstanceState != null) {
query = savedInstanceState.getString(QUERY);
search(query);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(QUERY, query);
}
private void setUpToolBar() {
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
// noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_search, menu);
final MenuItem searchItem = menu.findItem(R.id.search);
searchView = (SearchView) searchItem.getActionView();
searchView.setQueryHint(getString(R.string.action_search));
searchView.setMaxWidth(Integer.MAX_VALUE);
searchItem.expandActionView();
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
onBackPressed();
return false;
}
});
searchView.setQuery(query, false);
searchView.post(() -> searchView.setOnQueryTextListener(SearchActivity.this));
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
}
return super.onOptionsItemSelected(item);
}
private void search(@NonNull String query) {
this.query = query;
ItemQuery itemQuery = new ItemQuery();
itemQuery.setSearchTerm(query);
QueryUtil.getItems(itemQuery, new MediaCallback() {
@Override
public void onLoadMedia(List<?> media) {
Collections.sort(media, new Comparator<Object>() {
public int compare(Object one, Object two) {
if (one.getClass() == Album.class || one.getClass() == Artist.class) {
if (two.getClass() == Song.class) return -1;
}
if (two.getClass() == Album.class || two.getClass() == Artist.class) {
if (one.getClass() == Song.class) return 1;
}
return 0;
}
});
adapter.swapDataSet((List<Object>) media);
}
});
}
@Override
public void onMediaStoreChanged() {
super.onMediaStoreChanged();
}
@Override
public boolean onQueryTextSubmit(String query) {
hideSoftKeyboard();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
handler.removeCallbacksAndMessages(null);
handler.postDelayed(new Runnable() {
@Override
public void run() {
search(newText);
}
}, 1000);
return false;
}
private void hideSoftKeyboard() {
Util.hideSoftKeyboard(SearchActivity.this);
if (searchView != null) {
searchView.clearFocus();
}
}
}

View file

@ -0,0 +1,271 @@
package com.dkanada.gramophone.ui.activities;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.TwoStatePreference;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import com.afollestad.materialdialogs.color.ColorChooserDialog;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEColorPreference;
import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEPreferenceFragmentCompat;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.shortcuts.DynamicShortcutManager;
import com.dkanada.gramophone.preferences.LibraryPreference;
import com.dkanada.gramophone.preferences.LibraryPreferenceDialog;
import com.dkanada.gramophone.preferences.NowPlayingScreenPreference;
import com.dkanada.gramophone.preferences.NowPlayingScreenPreferenceDialog;
import com.dkanada.gramophone.ui.activities.base.AbsBaseActivity;
import com.dkanada.gramophone.util.PreferenceUtil;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SettingsActivity extends AbsBaseActivity implements ColorChooserDialog.ColorCallback {
@BindView(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preferences);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction().replace(R.id.content_frame, new SettingsFragment()).commit();
} else {
SettingsFragment frag = (SettingsFragment) getSupportFragmentManager().findFragmentById(R.id.content_frame);
if (frag != null) frag.invalidateSettings();
}
}
@Override
public void onColorSelection(@NonNull ColorChooserDialog dialog, @ColorInt int selectedColor) {
switch (dialog.getTitle()) {
case R.string.primary_color:
ThemeStore.editTheme(this).primaryColor(selectedColor).commit();
break;
case R.string.accent_color:
ThemeStore.editTheme(this).accentColor(selectedColor).commit();
break;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
new DynamicShortcutManager(this).updateDynamicShortcuts();
}
recreate();
}
@Override
public void onColorChooserDismissed(@NonNull ColorChooserDialog dialog) {
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public static class SettingsFragment extends ATEPreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static void setSummary(@NonNull Preference preference) {
setSummary(preference, PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
private static void setSummary(Preference preference, @NonNull Object value) {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else {
preference.setSummary(stringValue);
}
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.pref_library);
addPreferencesFromResource(R.xml.pref_colors);
addPreferencesFromResource(R.xml.pref_notification);
addPreferencesFromResource(R.xml.pref_now_playing_screen);
addPreferencesFromResource(R.xml.pref_lockscreen);
addPreferencesFromResource(R.xml.pref_audio);
}
@Nullable
@Override
public DialogFragment onCreatePreferenceDialog(Preference preference) {
if (preference instanceof NowPlayingScreenPreference) {
return NowPlayingScreenPreferenceDialog.newInstance();
} else if (preference instanceof LibraryPreference) {
return LibraryPreferenceDialog.newInstance();
}
return super.onCreatePreferenceDialog(preference);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getListView().setPadding(0, 0, 0, 0);
invalidateSettings();
PreferenceUtil.getInstance(getActivity()).registerOnSharedPreferenceChangedListener(this);
}
@Override
public void onDestroyView() {
super.onDestroyView();
PreferenceUtil.getInstance(getActivity()).unregisterOnSharedPreferenceChangedListener(this);
}
private void invalidateSettings() {
final Preference generalTheme = findPreference("general_theme");
setSummary(generalTheme);
generalTheme.setOnPreferenceChangeListener((preference, o) -> {
String themeName = (String) o;
setSummary(generalTheme, o);
ThemeStore.markChanged(getActivity());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
// Set the new theme so that updateAppShortcuts can pull it
getActivity().setTheme(PreferenceUtil.getThemeResFromPrefValue(themeName));
new DynamicShortcutManager(getActivity()).updateDynamicShortcuts();
}
getActivity().recreate();
return true;
});
final ATEColorPreference primaryColorPref = (ATEColorPreference) findPreference("primary_color");
final int primaryColor = ThemeStore.primaryColor(getActivity());
primaryColorPref.setColor(primaryColor, ColorUtil.darkenColor(primaryColor));
primaryColorPref.setOnPreferenceClickListener(preference -> {
new ColorChooserDialog.Builder(getActivity(), R.string.primary_color)
.accentMode(false)
.allowUserColorInput(true)
.allowUserColorInputAlpha(false)
.preselect(primaryColor)
.show(getActivity());
return true;
});
final ATEColorPreference accentColorPref = (ATEColorPreference) findPreference("accent_color");
final int accentColor = ThemeStore.accentColor(getActivity());
accentColorPref.setColor(accentColor, ColorUtil.darkenColor(accentColor));
accentColorPref.setOnPreferenceClickListener(preference -> {
new ColorChooserDialog.Builder(getActivity(), R.string.accent_color)
.accentMode(true)
.allowUserColorInput(true)
.allowUserColorInputAlpha(false)
.preselect(accentColor)
.show(getActivity());
return true;
});
TwoStatePreference colorNavBar = (TwoStatePreference) findPreference("should_color_navigation_bar");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
colorNavBar.setVisible(false);
} else {
colorNavBar.setChecked(ThemeStore.coloredNavigationBar(getActivity()));
colorNavBar.setOnPreferenceChangeListener((preference, newValue) -> {
ThemeStore.editTheme(getActivity())
.coloredNavigationBar((Boolean) newValue)
.commit();
getActivity().recreate();
return true;
});
}
final TwoStatePreference classicNotification = (TwoStatePreference) findPreference("classic_notification");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
classicNotification.setVisible(false);
} else {
classicNotification.setChecked(PreferenceUtil.getInstance(getActivity()).getClassicNotification());
classicNotification.setOnPreferenceChangeListener((preference, newValue) -> {
// Save preference
PreferenceUtil.getInstance(getActivity()).setClassicNotification((Boolean) newValue);
return true;
});
}
final TwoStatePreference coloredNotification = (TwoStatePreference) findPreference("colored_notification");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
coloredNotification.setEnabled(PreferenceUtil.getInstance(getActivity()).getClassicNotification());
} else {
coloredNotification.setChecked(PreferenceUtil.getInstance(getActivity()).getColoredNotification());
coloredNotification.setOnPreferenceChangeListener((preference, newValue) -> {
// Save preference
PreferenceUtil.getInstance(getActivity()).setColoredNotification((Boolean) newValue);
return true;
});
}
final TwoStatePreference colorAppShortcuts = (TwoStatePreference) findPreference("should_color_app_shortcuts");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
colorAppShortcuts.setVisible(false);
} else {
colorAppShortcuts.setChecked(PreferenceUtil.getInstance(getActivity()).getColoredShortcuts());
colorAppShortcuts.setOnPreferenceChangeListener((preference, newValue) -> {
// Save preference
PreferenceUtil.getInstance(getActivity()).setColoredShortcuts((Boolean) newValue);
// Update app shortcuts
new DynamicShortcutManager(getActivity()).updateDynamicShortcuts();
return true;
});
}
updateNowPlayingScreenSummary();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
switch (key) {
case PreferenceUtil.NOW_PLAYING_SCREEN:
updateNowPlayingScreenSummary();
break;
case PreferenceUtil.CLASSIC_NOTIFICATION:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference("colored_notification").setEnabled(sharedPreferences.getBoolean(key, false));
}
break;
}
}
private void updateNowPlayingScreenSummary() {
findPreference("now_playing_screen_id").setSummary(PreferenceUtil.getInstance(getActivity()).getNowPlayingScreen().titleRes);
}
}
}

View file

@ -0,0 +1,68 @@
package com.dkanada.gramophone.ui.activities;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.dkanada.gramophone.App;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.ui.activities.base.AbsBaseActivity;
import org.jellyfin.apiclient.interaction.AndroidCredentialProvider;
import org.jellyfin.apiclient.interaction.ConnectionResult;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
import org.jellyfin.apiclient.interaction.connectionmanager.ConnectionManager;
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.model.apiclient.ConnectionState;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
import org.jellyfin.apiclient.model.serialization.IJsonSerializer;
import butterknife.ButterKnife;
public class SplashActivity extends AbsBaseActivity {
public static final String TAG = SplashActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
setDrawUnderStatusbar();
ButterKnife.bind(this);
setStatusbarColorAuto();
setNavigationbarColorAuto();
setTaskDescriptionColorAuto();
IJsonSerializer jsonSerializer = new GsonJsonSerializer();
ILogger logger = new AndroidLogger(TAG);
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, this);
AndroidCredentialProvider credentialProvider = new AndroidCredentialProvider(jsonSerializer, this, logger);
if (credentialProvider.GetCredentials().getServers().size() == 0) {
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
} else {
final Context context = this;
ConnectionManager connectionManager = App.getConnectionManager(this, jsonSerializer, logger, httpClient);
connectionManager.Connect(credentialProvider.GetCredentials().getServers().get(0), new Response<ConnectionResult>() {
@Override
public void onResponse(ConnectionResult result) {
if (result.getState() != ConnectionState.SignedIn) {
Intent intent = new Intent(context, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
} else {
App.setApiClient(result.getApiClient());
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
}
}
});
}
}
}

View file

@ -0,0 +1,147 @@
package com.dkanada.gramophone.ui.activities.base;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.core.app.ActivityCompat;
import android.view.KeyEvent;
import android.view.View;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.dkanada.gramophone.R;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public abstract class AbsBaseActivity extends AbsThemeActivity {
public static final int PERMISSION_REQUEST = 100;
private boolean hadPermissions;
private String[] permissions;
private String permissionDeniedMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
permissions = getPermissionsToRequest();
hadPermissions = hasPermissions();
setPermissionDeniedMessage(null);
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
if (!hasPermissions()) {
requestPermissions();
}
}
@Override
protected void onResume() {
super.onResume();
final boolean hasPermissions = hasPermissions();
if (hasPermissions != hadPermissions) {
hadPermissions = hasPermissions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
onHasPermissionsChanged(hasPermissions);
}
}
}
protected void onHasPermissionsChanged(boolean hasPermissions) {
// implemented by sub classes
}
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU && event.getAction() == KeyEvent.ACTION_UP) {
showOverflowMenu();
return true;
}
return super.dispatchKeyEvent(event);
}
protected void showOverflowMenu() {
}
@Nullable
protected String[] getPermissionsToRequest() {
return null;
}
protected View getSnackBarContainer() {
return getWindow().getDecorView();
}
protected void setPermissionDeniedMessage(String message) {
permissionDeniedMessage = message;
}
private String getPermissionDeniedMessage() {
return permissionDeniedMessage == null ? getString(R.string.permissions_denied) : permissionDeniedMessage;
}
protected void requestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permissions != null) {
requestPermissions(permissions, PERMISSION_REQUEST);
}
}
protected boolean hasPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permissions != null) {
for (String permission : permissions) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(AbsBaseActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
//User has deny from permission dialog
Snackbar.make(getSnackBarContainer(), getPermissionDeniedMessage(),
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.action_grant, view -> requestPermissions())
.setActionTextColor(ThemeStore.accentColor(this))
.show();
} else {
// User has deny permission and checked never show permission dialog so you can redirect to Application settings page
Snackbar.make(getSnackBarContainer(), getPermissionDeniedMessage(),
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.action_settings, view -> {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", AbsBaseActivity.this.getPackageName(), null);
intent.setData(uri);
startActivity(intent);
})
.setActionTextColor(ThemeStore.accentColor(this))
.show();
}
return;
}
}
hadPermissions = true;
onHasPermissionsChanged(true);
}
}
}

View file

@ -0,0 +1,218 @@
package com.dkanada.gramophone.ui.activities.base;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.interfaces.MusicServiceEventListener;
import com.dkanada.gramophone.service.MusicService;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public abstract class AbsMusicServiceActivity extends AbsBaseActivity implements MusicServiceEventListener {
private final List<MusicServiceEventListener> mMusicServiceEventListeners = new ArrayList<>();
private MusicPlayerRemote.ServiceToken serviceToken;
private MusicStateReceiver musicStateReceiver;
private boolean receiverRegistered;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
serviceToken = MusicPlayerRemote.bindToService(this, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
AbsMusicServiceActivity.this.onServiceConnected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
AbsMusicServiceActivity.this.onServiceDisconnected();
}
});
setPermissionDeniedMessage(getString(R.string.permission_external_storage_denied));
}
@Override
protected void onDestroy() {
super.onDestroy();
MusicPlayerRemote.unbindFromService(serviceToken);
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver);
receiverRegistered = false;
}
}
public void addMusicServiceEventListener(final MusicServiceEventListener listener) {
if (listener != null) {
mMusicServiceEventListeners.add(listener);
}
}
public void removeMusicServiceEventListener(final MusicServiceEventListener listener) {
if (listener != null) {
mMusicServiceEventListeners.remove(listener);
}
}
@Override
public void onServiceConnected() {
if (!receiverRegistered) {
musicStateReceiver = new MusicStateReceiver(this);
final IntentFilter filter = new IntentFilter();
filter.addAction(MusicService.PLAY_STATE_CHANGED);
filter.addAction(MusicService.SHUFFLE_MODE_CHANGED);
filter.addAction(MusicService.REPEAT_MODE_CHANGED);
filter.addAction(MusicService.META_CHANGED);
filter.addAction(MusicService.QUEUE_CHANGED);
filter.addAction(MusicService.MEDIA_STORE_CHANGED);
registerReceiver(musicStateReceiver, filter);
receiverRegistered = true;
}
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onServiceConnected();
}
}
}
@Override
public void onServiceDisconnected() {
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver);
receiverRegistered = false;
}
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onServiceDisconnected();
}
}
}
@Override
public void onPlayingMetaChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onPlayingMetaChanged();
}
}
}
@Override
public void onQueueChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onQueueChanged();
}
}
}
@Override
public void onPlayStateChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onPlayStateChanged();
}
}
}
@Override
public void onMediaStoreChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onMediaStoreChanged();
}
}
}
@Override
public void onRepeatModeChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onRepeatModeChanged();
}
}
}
@Override
public void onShuffleModeChanged() {
for (MusicServiceEventListener listener : mMusicServiceEventListeners) {
if (listener != null) {
listener.onShuffleModeChanged();
}
}
}
private static final class MusicStateReceiver extends BroadcastReceiver {
private final WeakReference<AbsMusicServiceActivity> reference;
public MusicStateReceiver(final AbsMusicServiceActivity activity) {
reference = new WeakReference<>(activity);
}
@Override
public void onReceive(final Context context, @NonNull final Intent intent) {
final String action = intent.getAction();
AbsMusicServiceActivity activity = reference.get();
if (activity != null) {
switch (action) {
case MusicService.META_CHANGED:
activity.onPlayingMetaChanged();
break;
case MusicService.QUEUE_CHANGED:
activity.onQueueChanged();
break;
case MusicService.PLAY_STATE_CHANGED:
activity.onPlayStateChanged();
break;
case MusicService.REPEAT_MODE_CHANGED:
activity.onRepeatModeChanged();
break;
case MusicService.SHUFFLE_MODE_CHANGED:
activity.onShuffleModeChanged();
break;
case MusicService.MEDIA_STORE_CHANGED:
activity.onMediaStoreChanged();
break;
}
}
}
}
@Override
protected void onHasPermissionsChanged(boolean hasPermissions) {
super.onHasPermissionsChanged(hasPermissions);
Intent intent = new Intent(MusicService.MEDIA_STORE_CHANGED);
intent.putExtra("from_permissions_changed", true); // just in case we need to know this at some point
sendBroadcast(intent);
}
@Nullable
@Override
protected String[] getPermissionsToRequest() {
return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
}

View file

@ -0,0 +1,304 @@
package com.dkanada.gramophone.ui.activities.base;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.LayoutRes;
import androidx.fragment.app.Fragment;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.PathInterpolator;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.helper.MusicPlayerRemote;
import com.dkanada.gramophone.ui.fragments.player.AbsPlayerFragment;
import com.dkanada.gramophone.ui.fragments.player.MiniPlayerFragment;
import com.dkanada.gramophone.ui.fragments.player.NowPlayingScreen;
import com.dkanada.gramophone.ui.fragments.player.card.CardPlayerFragment;
import com.dkanada.gramophone.ui.fragments.player.flat.FlatPlayerFragment;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.dkanada.gramophone.util.ViewUtil;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* @author Karim Abou Zeid (kabouzeid)
* <p/>
* Do not use {@link #setContentView(int)}. Instead wrap your layout with
* {@link #wrapSlidingMusicPanel(int)} first and then return it in {@link #createContentView()}
*/
public abstract class AbsSlidingMusicPanelActivity extends AbsMusicServiceActivity implements SlidingUpPanelLayout.PanelSlideListener, CardPlayerFragment.Callbacks {
@BindView(R.id.sliding_layout)
SlidingUpPanelLayout slidingUpPanelLayout;
private int navigationbarColor;
private int taskColor;
private boolean lightStatusbar;
private NowPlayingScreen currentNowPlayingScreen;
private AbsPlayerFragment playerFragment;
private MiniPlayerFragment miniPlayerFragment;
private ValueAnimator navigationBarColorAnimator;
private ArgbEvaluator argbEvaluator = new ArgbEvaluator();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(createContentView());
ButterKnife.bind(this);
currentNowPlayingScreen = PreferenceUtil.getInstance(this).getNowPlayingScreen();
Fragment fragment; // must implement AbsPlayerFragment
switch (currentNowPlayingScreen) {
case FLAT:
fragment = new FlatPlayerFragment();
break;
case CARD:
default:
fragment = new CardPlayerFragment();
break;
}
getSupportFragmentManager().beginTransaction().replace(R.id.player_fragment_container, fragment).commit();
getSupportFragmentManager().executePendingTransactions();
playerFragment = (AbsPlayerFragment) getSupportFragmentManager().findFragmentById(R.id.player_fragment_container);
miniPlayerFragment = (MiniPlayerFragment) getSupportFragmentManager().findFragmentById(R.id.mini_player_fragment);
// noinspection ConstantConditions
miniPlayerFragment.getView().setOnClickListener(v -> expandPanel());
slidingUpPanelLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
slidingUpPanelLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
switch (getPanelState()) {
case EXPANDED:
onPanelSlide(slidingUpPanelLayout, 1);
onPanelExpanded(slidingUpPanelLayout);
break;
case COLLAPSED:
onPanelCollapsed(slidingUpPanelLayout);
break;
default:
playerFragment.onHide();
break;
}
}
});
slidingUpPanelLayout.addPanelSlideListener(this);
}
@Override
protected void onResume() {
super.onResume();
if (currentNowPlayingScreen != PreferenceUtil.getInstance(this).getNowPlayingScreen()) {
postRecreate();
}
}
public void setAntiDragView(View antiDragView) {
slidingUpPanelLayout.setAntiDragView(antiDragView);
}
protected abstract View createContentView();
@Override
public void onServiceConnected() {
super.onServiceConnected();
if (!MusicPlayerRemote.getPlayingQueue().isEmpty()) {
slidingUpPanelLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
slidingUpPanelLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
hideBottomBar(false);
}
});
} // don't call hideBottomBar(true) here as it causes a bug with the SlidingUpPanelLayout
}
@Override
public void onQueueChanged() {
super.onQueueChanged();
hideBottomBar(MusicPlayerRemote.getPlayingQueue().isEmpty());
}
@Override
public void onPanelSlide(View panel, @FloatRange(from = 0, to = 1) float slideOffset) {
setMiniPlayerAlphaProgress(slideOffset);
if (navigationBarColorAnimator != null) navigationBarColorAnimator.cancel();
super.setNavigationbarColor((int) argbEvaluator.evaluate(slideOffset, navigationbarColor, playerFragment.getPaletteColor()));
}
@Override
public void onPanelStateChanged(View panel, SlidingUpPanelLayout.PanelState previousState, SlidingUpPanelLayout.PanelState newState) {
switch (newState) {
case COLLAPSED:
onPanelCollapsed(panel);
break;
case EXPANDED:
onPanelExpanded(panel);
break;
case ANCHORED:
collapsePanel(); // this fixes a bug where the panel would get stuck for some reason
break;
}
}
public void onPanelCollapsed(View panel) {
// restore values
super.setLightStatusbar(lightStatusbar);
super.setTaskDescriptionColor(taskColor);
super.setNavigationbarColor(navigationbarColor);
playerFragment.setMenuVisibility(false);
playerFragment.setUserVisibleHint(false);
playerFragment.onHide();
}
public void onPanelExpanded(View panel) {
// setting fragments values
int playerFragmentColor = playerFragment.getPaletteColor();
super.setLightStatusbar(false);
super.setTaskDescriptionColor(playerFragmentColor);
super.setNavigationbarColor(playerFragmentColor);
playerFragment.setMenuVisibility(true);
playerFragment.setUserVisibleHint(true);
playerFragment.onShow();
}
private void setMiniPlayerAlphaProgress(@FloatRange(from = 0, to = 1) float progress) {
if (miniPlayerFragment.getView() == null) return;
float alpha = 1 - progress;
miniPlayerFragment.getView().setAlpha(alpha);
// necessary to make the views below clickable
miniPlayerFragment.getView().setVisibility(alpha == 0 ? View.GONE : View.VISIBLE);
}
public SlidingUpPanelLayout.PanelState getPanelState() {
return slidingUpPanelLayout == null ? null : slidingUpPanelLayout.getPanelState();
}
public void collapsePanel() {
slidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
}
public void expandPanel() {
slidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED);
}
public void hideBottomBar(final boolean hide) {
if (hide) {
slidingUpPanelLayout.setPanelHeight(0);
collapsePanel();
} else {
slidingUpPanelLayout.setPanelHeight(getResources().getDimensionPixelSize(R.dimen.mini_player_height));
}
}
protected View wrapSlidingMusicPanel(@LayoutRes int resId) {
@SuppressLint("InflateParams")
View slidingMusicPanelLayout = getLayoutInflater().inflate(R.layout.sliding_music_panel_layout, null);
ViewGroup contentContainer = slidingMusicPanelLayout.findViewById(R.id.content_container);
getLayoutInflater().inflate(resId, contentContainer);
return slidingMusicPanelLayout;
}
@Override
public void onBackPressed() {
if (!handleBackPress())
super.onBackPressed();
}
public boolean handleBackPress() {
if (slidingUpPanelLayout.getPanelHeight() != 0 && playerFragment.onBackPressed())
return true;
if (getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) {
collapsePanel();
return true;
}
return false;
}
@Override
public void onPaletteColorChanged() {
if (getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) {
int playerFragmentColor = playerFragment.getPaletteColor();
super.setTaskDescriptionColor(playerFragmentColor);
animateNavigationBarColor(playerFragmentColor);
}
}
@Override
public void setLightStatusbar(boolean enabled) {
lightStatusbar = enabled;
if (getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
super.setLightStatusbar(enabled);
}
}
@Override
public void setNavigationbarColor(int color) {
this.navigationbarColor = color;
if (getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
if (navigationBarColorAnimator != null) navigationBarColorAnimator.cancel();
super.setNavigationbarColor(color);
}
}
private void animateNavigationBarColor(int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (navigationBarColorAnimator != null) navigationBarColorAnimator.cancel();
navigationBarColorAnimator = ValueAnimator
.ofArgb(getWindow().getNavigationBarColor(), color)
.setDuration(ViewUtil.PHONOGRAPH_ANIM_TIME);
navigationBarColorAnimator.setInterpolator(new PathInterpolator(0.4f, 0f, 1f, 1f));
navigationBarColorAnimator.addUpdateListener(animation -> AbsSlidingMusicPanelActivity.super.setNavigationbarColor((Integer) animation.getAnimatedValue()));
navigationBarColorAnimator.start();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (navigationBarColorAnimator != null) navigationBarColorAnimator.cancel(); // just in case
}
@Override
public void setTaskDescriptionColor(@ColorInt int color) {
this.taskColor = color;
if (getPanelState() == null || getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
super.setTaskDescriptionColor(color);
}
}
@Override
protected View getSnackBarContainer() {
return findViewById(R.id.content_container);
}
public SlidingUpPanelLayout getSlidingUpPanelLayout() {
return slidingUpPanelLayout;
}
public MiniPlayerFragment getMiniPlayerFragment() {
return miniPlayerFragment;
}
public AbsPlayerFragment getPlayerFragment() {
return playerFragment;
}
}

View file

@ -0,0 +1,93 @@
package com.dkanada.gramophone.ui.activities.base;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.ColorInt;
import android.view.View;
import com.kabouzeid.appthemehelper.ATH;
import com.kabouzeid.appthemehelper.ThemeStore;
import com.kabouzeid.appthemehelper.common.ATHToolbarActivity;
import com.kabouzeid.appthemehelper.util.ColorUtil;
import com.kabouzeid.appthemehelper.util.MaterialDialogsUtil;
import com.dkanada.gramophone.R;
import com.dkanada.gramophone.util.PreferenceUtil;
import com.dkanada.gramophone.util.Util;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public abstract class AbsThemeActivity extends ATHToolbarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(PreferenceUtil.getInstance(this).getGeneralTheme());
super.onCreate(savedInstanceState);
MaterialDialogsUtil.updateMaterialDialogsThemeSingleton(this);
}
protected void setDrawUnderStatusbar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
Util.setAllowDrawUnderStatusBar(getWindow());
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
Util.setStatusBarTranslucent(getWindow());
}
/**
* This will set the color of the view with the id "status_bar" on KitKat and Lollipop.
* On Lollipop if no such view is found it will set the statusbar color using the native method.
*
* @param color the new statusbar color (will be shifted down on Lollipop and above)
*/
public void setStatusbarColor(int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final View statusBar = getWindow().getDecorView().getRootView().findViewById(R.id.status_bar);
if (statusBar != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
statusBar.setBackgroundColor(ColorUtil.darkenColor(color));
setLightStatusbarAuto(color);
} else {
statusBar.setBackgroundColor(color);
}
} else if (Build.VERSION.SDK_INT >= 21) {
getWindow().setStatusBarColor(ColorUtil.darkenColor(color));
setLightStatusbarAuto(color);
}
}
}
public void setStatusbarColorAuto() {
// we don't want to use statusbar color because we are doing the color darkening on our own to support KitKat
setStatusbarColor(ThemeStore.primaryColor(this));
}
public void setTaskDescriptionColor(@ColorInt int color) {
ATH.setTaskDescriptionColor(this, color);
}
public void setTaskDescriptionColorAuto() {
setTaskDescriptionColor(ThemeStore.primaryColor(this));
}
public void setNavigationbarColor(int color) {
if (ThemeStore.coloredNavigationBar(this)) {
ATH.setNavigationbarColor(this, color);
} else {
ATH.setNavigationbarColor(this, Color.BLACK);
}
}
public void setNavigationbarColorAuto() {
setNavigationbarColor(ThemeStore.navigationBarColor(this));
}
public void setLightStatusbar(boolean enabled) {
ATH.setLightStatusbar(this, enabled);
}
public void setLightStatusbarAuto(int bgColor) {
setLightStatusbar(ColorUtil.isColorLight(bgColor));
}
}

Some files were not shown because too many files have changed in this diff Show more