diff options
Diffstat (limited to 'app/src/main/java')
82 files changed, 3264 insertions, 1104 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java index a58f169b..06559456 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java @@ -33,7 +33,6 @@ import android.os.Environment; import android.os.Handler; import android.support.design.widget.NavigationView; import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; @@ -67,7 +66,9 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.AdminFragment; import github.daneren2005.dsub.fragments.SubsonicFragment; +import github.daneren2005.dsub.fragments.UserFragment; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.HeadphoneListenerService; import github.daneren2005.dsub.service.MusicService; @@ -189,9 +190,8 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void createCustomActionBarView() { - View customActionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null); - actionBarSpinner = (Spinner)customActionbar.findViewById(R.id.spinner); - if(Util.getThemeRes(this) == R.style.Theme_DSub_Light_No_Actionbar) { + actionBarSpinner = (Spinner) getLayoutInflater().inflate(R.layout.actionbar_spinner, null); + if((this instanceof SubsonicFragmentActivity || this instanceof SettingsActivity) && (Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true) || Util.getThemeRes(this) != R.style.Theme_DSub_Light_No_Color)) { actionBarSpinner.setBackgroundResource(R.drawable.abc_spinner_mtrl_am_alpha); } spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); @@ -199,7 +199,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte actionBarSpinner.setOnItemSelectedListener(this); actionBarSpinner.setAdapter(spinnerAdapter); - getSupportActionBar().setCustomView(customActionbar); + getSupportActionBar().setCustomView(actionBarSpinner); } @Override @@ -538,7 +538,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { int top = spinnerAdapter.getCount() - 1; if(position < top) { - for(int i = top; i > position; i--) { + for(int i = top; i > position && i >= 0; i--) { removeCurrent(); } } @@ -752,6 +752,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte recreateSpinner(); } public void removeCurrent() { + // Don't try to remove current if there is no backstack to remove from + if(backStack.isEmpty()) { + return; + } + if(currentFragment != null) { currentFragment.setPrimaryFragment(false); } @@ -819,7 +824,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte removeCurrent(); } - currentFragment.invalidate(); + if(currentFragment instanceof UserFragment || currentFragment instanceof AdminFragment) { + restart(false); + } else { + currentFragment.invalidate(); + } populateTabs(); } @@ -853,6 +862,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte spinnerAdapter.notifyDataSetChanged(); actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1); if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setDisplayShowCustomEnabled(true); } @@ -862,6 +872,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } else if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(true); getSupportActionBar().setTitle(currentFragment.getTitle()); getSupportActionBar().setDisplayShowCustomEnabled(false); drawerToggle.setDrawerIndicatorEnabled(true); @@ -869,10 +880,19 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void restart() { - Intent intent = new Intent(this, ((Object) this).getClass()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + restart(true); + } + protected void restart(boolean resumePosition) { + Intent intent = new Intent(this, this.getClass()); intent.putExtras(getIntent()); - intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + if(resumePosition) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } else { + String fragmentType = Util.openToTab(this); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + intent.putExtra(Constants.FRAGMENT_POSITION, getDrawerItemId(fragmentType)); + } + finish(); Util.startActivityWithoutTransition(this, intent); } @@ -1044,6 +1064,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte UserUtil.seedCurrentUser(this); this.updateDrawerHeader(); + drawer.closeDrawers(); } private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) { diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 87a67e80..17cfdb42 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -93,6 +93,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private SlidingUpPanelLayout slideUpPanel; private SlidingUpPanelLayout.PanelSlideListener panelSlideListener; + private boolean isPanelClosing = false; private NowPlayingFragment nowPlayingFragment; private SubsonicFragment secondaryFragment; private Toolbar mainToolbar; @@ -106,6 +107,10 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private long lastBackPressTime = 0; private DownloadFile currentPlaying; private PlayerState currentState; + private ImageButton previousButton; + private ImageButton nextButton; + private ImageButton rewindButton; + private ImageButton fastforwardButton; @Override public void onCreate(Bundle savedInstanceState) { @@ -153,8 +158,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo if(item != null) { item.setChecked(true); } + } else { + lastSelectedPosition = getDrawerItemId(fragmentType); } + currentFragment = getNewFragment(fragmentType); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ID)) { + Bundle currentArguments = currentFragment.getArguments(); + if(currentArguments == null) { + currentArguments = new Bundle(); + } + currentArguments.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + currentFragment.setArguments(currentArguments); + } currentFragment.setPrimaryFragment(true); getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); @@ -187,19 +203,24 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void onPanelCollapsed(View panel) { - bottomBar.setVisibility(View.VISIBLE); - nowPlayingToolbar.setVisibility(View.GONE); - nowPlayingFragment.setPrimaryFragment(false); - setSupportActionBar(mainToolbar); - recreateSpinner(); + isPanelClosing = false; + if(bottomBar.getVisibility() == View.GONE) { + bottomBar.setVisibility(View.VISIBLE); + nowPlayingToolbar.setVisibility(View.GONE); + nowPlayingFragment.setPrimaryFragment(false); + setSupportActionBar(mainToolbar); + recreateSpinner(); + } } @Override public void onPanelExpanded(View panel) { + isPanelClosing = false; currentFragment.stopActionMode(); // Disable custom view before switching getSupportActionBar().setDisplayShowCustomEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(true); bottomBar.setVisibility(View.GONE); nowPlayingToolbar.setVisibility(View.VISIBLE); @@ -230,6 +251,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo openNowPlaying(); } }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); } bottomBar = findViewById(R.id.bottom_bar); @@ -248,7 +271,25 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo trans.commit(); } - ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous); + rewindButton = (ImageButton) findViewById(R.id.download_rewind); + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().rewind(); + return null; + } + }.execute(); + } + }); + + previousButton = (ImageButton) findViewById(R.id.download_previous); previousButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -286,7 +327,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } }); - ImageButton nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton = (ImageButton) findViewById(R.id.download_next); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -303,6 +344,24 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo }.execute(); } }); + + fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward); + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().fastForward(); + return null; + } + }.execute(); + } + }); } @Override @@ -330,17 +389,16 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo super.onNewIntent(intent); if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + if(currentFragment instanceof SearchFragment) { String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); - boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false); if (query != null) { ((SearchFragment)currentFragment).search(query, autoplay); - } else { - if (requestsearch) { - onSearchRequested(); - } } getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY); } else { @@ -349,7 +407,14 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo SearchFragment fragment = new SearchFragment(); replaceFragment(fragment, fragment.getSupportTag()); } + } else if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, false)) { + if(slideUpPanel.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) { + openNowPlaying(); + } } else { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } setIntent(intent); } if(drawer != null) { @@ -472,7 +537,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { - if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) { secondaryFragment = fragment; nowPlayingFragment.setPrimaryFragment(false); secondaryFragment.setPrimaryFragment(true); @@ -562,6 +627,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void closeNowPlaying() { slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; } private SubsonicFragment getNewFragment(String fragmentType) { @@ -851,13 +917,26 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } getImageLoader().loadImage(coverArtView, song, false, height, false); } + + if(currentPlaying != null && currentPlaying.getSong() != null && (currentPlaying.getSong().isPodcast() || currentPlaying.getSong().isAudioBook())) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } } @Override public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) { if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { onSongChanged(currentPlaying, currentPlayingIndex); - onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); } } @@ -875,7 +954,21 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } @Override - public void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange) { + public void onMetadataUpdate(MusicDirectory.Entry song, int fieldChange) { + if(song != null && coverArtView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + // We need to update it immediately since it won't update if updater is not running for it + if(nowPlayingFragment != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) { + nowPlayingFragment.onMetadataUpdate(song, fieldChange); + } + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java index 5feaa482..5ed79e82 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java @@ -23,27 +23,32 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import java.io.Serializable; import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.ArtistView; import github.daneren2005.dsub.view.FastScroller; +import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; -public class ArtistAdapter extends SectionAdapter<Artist> implements FastScroller.BubbleTextGetter { +public class ArtistAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_SONG = 3; public static int VIEW_TYPE_ARTIST = 4; private List<MusicFolder> musicFolders; private OnMusicFolderChanged onMusicFolderChanged; - public ArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener listener) { + public ArtistAdapter(Context context, List<Serializable> artists, OnItemClickedListener listener) { this(context, artists, null, listener, null); } - public ArtistAdapter(Context context, List<Artist> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { + public ArtistAdapter(Context context, List<Serializable> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { super(context, artists); this.musicFolders = musicFolders; this.onItemClickedListener = onItemClickedListener; @@ -92,7 +97,7 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle return new UpdateView.UpdateViewHolder(header, false); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { TextView folderName = (TextView) holder.getView().findViewById(R.id.select_artist_folder_2); String musicFolderId = Util.getSelectedMusicFolderId(context); @@ -110,17 +115,35 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new ArtistView(context)); + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); } @Override - public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { - holder.getUpdateView().setObject(item); + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + Entry entry = (Entry) item; + songView.setObject(entry, checkable && !entry.isVideo()); + } } @Override - public int getItemViewType(Artist item) { - return VIEW_TYPE_ARTIST; + public int getItemViewType(Serializable item) { + if(item instanceof Artist) { + return VIEW_TYPE_ARTIST; + } else { + return VIEW_TYPE_SONG; + } } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java index d4613994..5b3dc289 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java @@ -16,13 +16,18 @@ package github.daneren2005.dsub.adapter; import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import java.util.List; +import github.daneren2005.dsub.R; import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.FastScroller; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -33,6 +38,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public DownloadFileAdapter(Context context, List<DownloadFile> entries, OnItemClickedListener onItemClickedListener) { super(context, entries); this.onItemClickedListener = onItemClickedListener; + this.checkable = true; } @Override @@ -43,7 +49,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements @Override public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) { SongView songView = (SongView) holder.getUpdateView(); - songView.setObject(item.getSong(), false); + songView.setObject(item.getSong(), Util.isBatchMode(context)); songView.setDownloadFile(item); } @@ -56,4 +62,21 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public String getTextToShowInBubble(int position) { return null; } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_nowplaying, menu); + } + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).getSong().isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java index 38931482..e75a5104 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java @@ -16,13 +16,12 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; @@ -45,8 +44,8 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { private ImageLoader imageLoader; private boolean largeAlbums; private boolean showArtist = false; + private boolean showAlbum = false; private boolean removeFromPlaylist = false; - private boolean removeStarred = true; private View header; public EntryGridAdapter(Context context, List<Entry> entries, ImageLoader imageLoader, boolean largeCell) { @@ -89,6 +88,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { albumView.setObject(entry, imageLoader); } else if(viewType == VIEW_TYPE_SONG) { SongView songView = (SongView) view; + songView.setShowAlbum(showAlbum); songView.setObject(entry, checkable && !entry.isVideo()); } } @@ -96,7 +96,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { } @@ -125,6 +125,10 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { this.showArtist = showArtist; } + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } + public void removeAt(int index) { sections.get(0).remove(index); if(header != null) { @@ -136,9 +140,6 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public void setRemoveFromPlaylist(boolean removeFromPlaylist) { this.removeFromPlaylist = removeFromPlaylist; } - public void setRemoveStarred(boolean removeStarred) { - this.removeStarred = removeStarred; - } @Override public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { @@ -151,8 +152,13 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { if(!removeFromPlaylist) { menu.removeItem(R.id.menu_remove_playlist); } - if(removeStarred) { - menu.removeItem(R.id.menu_unstar); + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } } } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java index 2c4f75dc..6c1c14da 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java @@ -26,6 +26,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.MainFragment; import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ImageLoader; @@ -88,6 +89,10 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { this.type = type; this.extra = extra; this.size = size; + + if(super.getItemCount() < size) { + allLoaded = true; + } } public void loadMore() { @@ -110,7 +115,7 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { appendCachedData(newData); loading = false; - if(newData.isEmpty()) { + if(newData.size() < size) { allLoaded = true; notifyDataSetChanged(); } @@ -126,6 +131,8 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { result = service.getAlbumList(type, extra, size, offset, false, context, null); } else if("genres".equals(type) || "genres-songs".equals(type)) { result = service.getSongsByGenre(extra, size, offset, context, null); + }else if(type.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(type, size, offset, context, null); } else { result = service.getAlbumList(type, size, offset, false, context, null); } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java new file mode 100644 index 00000000..6ebb34e3 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java @@ -0,0 +1,150 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + Copyright 2015 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.view.BasicHeaderView; +import github.daneren2005.dsub.view.UpdateView; + +public abstract class ExpandableSectionAdapter<T> extends SectionAdapter<T> { + private static final String TAG = ExpandableSectionAdapter.class.getSimpleName(); + private static final int DEFAULT_VISIBLE = 4; + private static final int EXPAND_TOGGLE = R.attr.select_server; + private static final int COLLAPSE_TOGGLE = R.attr.select_tabs; + + protected List<Integer> sectionsDefaultVisible; + protected List<List<T>> sectionsExtras; + protected int expandToggleRes; + protected int collapseToggleRes; + + protected ExpandableSectionAdapter() {} + public ExpandableSectionAdapter(Context context, List<T> section) { + List<List<T>> sections = new ArrayList<>(); + sections.add(section); + + init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null)); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections) { + init(context, headers, sections, null); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections, List<Integer> sectionsDefaultVisible) { + init(context, headers, sections, sectionsDefaultVisible); + } + protected void init(Context context, List<String> headers, List<List<T>> fullSections, List<Integer> sectionsDefaultVisible) { + this.context = context; + this.headers = headers; + this.sectionsDefaultVisible = sectionsDefaultVisible; + if(sectionsDefaultVisible == null) { + sectionsDefaultVisible = new ArrayList<>(fullSections.size()); + for(int i = 0; i < fullSections.size(); i++) { + sectionsDefaultVisible.add(DEFAULT_VISIBLE); + } + } + + this.sections = new ArrayList<>(); + this.sectionsExtras = new ArrayList<>(); + int i = 0; + for(List<T> fullSection: fullSections) { + List<T> visibleSection = new ArrayList<>(); + + Integer defaultVisible = sectionsDefaultVisible.get(i); + if(defaultVisible == null || defaultVisible >= fullSection.size()) { + visibleSection.addAll(fullSection); + this.sectionsExtras.add(null); + } else { + visibleSection.addAll(fullSection.subList(0, defaultVisible)); + this.sectionsExtras.add(fullSection.subList(defaultVisible, fullSection.size())); + } + this.sections.add(visibleSection); + + i++; + } + + expandToggleRes = DrawableTint.getDrawableRes(context, EXPAND_TOGGLE); + collapseToggleRes = DrawableTint.getDrawableRes(context, COLLAPSE_TOGGLE); + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.expandable_header)); + } + + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, final int sectionIndex) { + UpdateView view = holder.getUpdateView(); + ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); + + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + if(sectionExtras != null && !sectionExtras.isEmpty()) { + toggleSelectionView.setVisibility(View.VISIBLE); + toggleSelectionView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + // Update icon + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = COLLAPSE_TOGGLE; + + // Update how many are displayed + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + visibleSelection.addAll(sectionExtras); + notifyItemRangeInserted(lastIndex, sectionExtras.size()); + } else { + selectToggleAttr = EXPAND_TOGGLE; + + // Update how many are displayed + visibleSelection.removeAll(sectionExtras); + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + notifyItemRangeRemoved(lastIndex, sectionExtras.size()); + } + + ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } + }); + + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = EXPAND_TOGGLE; + } else { + selectToggleAttr = COLLAPSE_TOGGLE; + } + + toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } else { + toggleSelectionView.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(header); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java index 8f1f1c38..dd70aa99 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java @@ -77,7 +77,7 @@ public class MainAdapter extends SectionAdapter<Integer> { return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header)); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox); @@ -100,6 +100,9 @@ public class MainAdapter extends SectionAdapter<Integer> { } else if("videos".equals(header)) { display = context.getResources().getString(R.string.main_videos); checkBox.setVisibility(View.GONE); + } else if("songs".equals(header)) { + display = context.getResources().getString(R.string.search_songs); + checkBox.setVisibility(View.GONE); } else { display = header; checkBox.setVisibility(View.GONE); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java index a95abeda..f843a722 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java @@ -34,21 +34,17 @@ import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; import java.io.Serializable; +import java.util.Arrays; import java.util.List; -public class PodcastChannelAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { +public class PodcastChannelAdapter extends ExpandableSectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { public static final int VIEW_TYPE_PODCAST_LEGACY = 1; public static final int VIEW_TYPE_PODCAST_LINE = 2; public static final int VIEW_TYPE_PODCAST_CELL = 3; public static final int VIEW_TYPE_PODCAST_EPISODE = 4; - public static final String EPISODE_HEADER = "episodes"; - public static final String CHANNEL_HEADER = "channels"; - private ImageLoader imageLoader; private boolean largeCell; - private int selectToggleAttr = R.attr.select_server; - private List<Serializable> extraEpisodes; public PodcastChannelAdapter(Context context, List<Serializable> podcasts, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { super(context, podcasts); @@ -56,9 +52,8 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen this.onItemClickedListener = listener; this.largeCell = largeCell; } - public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, List<Serializable> extraEpisodes, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { - super(context, headers, sections); - this.extraEpisodes = extraEpisodes; + public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { + super(context, headers, sections, Arrays.asList(3, null)); this.imageLoader = imageLoader; this.onItemClickedListener = listener; this.largeCell = largeCell; @@ -126,58 +121,6 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen } menu.removeItem(R.id.menu_remove_playlist); - menu.removeItem(R.id.menu_unstar); - } - - @Override - public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { - return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.newest_episode_header)); - } - - @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { - UpdateView view = holder.getUpdateView(); - ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); - - String display; - if(EPISODE_HEADER.equals(header)) { - display = context.getResources().getString(R.string.main_albums_newest); - - if(extraEpisodes != null && !extraEpisodes.isEmpty()) { - toggleSelectionView.setVisibility(View.VISIBLE); - toggleSelectionView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Update icon - if (selectToggleAttr == R.attr.select_server) { - selectToggleAttr = R.attr.select_tabs; - - // Update how many are displayed - sections.get(0).addAll(extraEpisodes); - notifyItemRangeInserted(4, extraEpisodes.size()); - } else { - selectToggleAttr = R.attr.select_server; - - // Update how many are displayed - sections.get(0).removeAll(extraEpisodes); - notifyItemRangeRemoved(4, extraEpisodes.size()); - } - - ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - - } - }); - toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - } else { - toggleSelectionView.setVisibility(View.GONE); - } - } else { - display = context.getResources().getString(R.string.select_podcasts_channels); - toggleSelectionView.setVisibility(View.GONE); - } - - if(view != null) { - view.setObject(display); - } + menu.removeItem(R.id.menu_star); } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java index 66f2db21..69e5d56d 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java @@ -19,7 +19,9 @@ import android.content.Context; import android.content.res.Resources; import android.view.Menu; import android.view.MenuInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import java.io.Serializable; import java.util.ArrayList; @@ -28,10 +30,12 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.AlbumView; import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -40,32 +44,39 @@ import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_C import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE; import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_SONG; -public class SearchAdapter extends SectionAdapter<Serializable> { - private SearchResult searchResult; +public class SearchAdapter extends ExpandableSectionAdapter<Serializable> { private ImageLoader imageLoader; private boolean largeAlbums; + private static final int MAX_ARTISTS = 10; + private static final int MAX_ALBUMS = 4; + private static final int MAX_SONGS = 10; + public SearchAdapter(Context context, SearchResult searchResult, ImageLoader imageLoader, boolean largeAlbums, OnItemClickedListener listener) { - this.context = context; - this.searchResult = searchResult; this.imageLoader = imageLoader; this.largeAlbums = largeAlbums; - this.sections = new ArrayList<>(); - this.headers = new ArrayList<>(); + List<List<Serializable>> sections = new ArrayList<>(); + List<String> headers = new ArrayList<>(); + List<Integer> defaultVisible = new ArrayList<>(); Resources res = context.getResources(); if(!searchResult.getArtists().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); - this.headers.add(res.getString(R.string.search_artists)); + sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); + headers.add(res.getString(R.string.search_artists)); + defaultVisible.add(MAX_ARTISTS); } if(!searchResult.getAlbums().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); - this.headers.add(res.getString(R.string.search_albums)); + sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); + headers.add(res.getString(R.string.search_albums)); + defaultVisible.add(MAX_ALBUMS); } if(!searchResult.getSongs().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); - this.headers.add(res.getString(R.string.search_songs)); + sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); + headers.add(res.getString(R.string.search_songs)); + defaultVisible.add(MAX_SONGS); } + init(context, headers, sections, defaultVisible); + this.onItemClickedListener = listener; checkable = true; } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java index d5f9a6ea..33bbb384 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java @@ -194,7 +194,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH for(List<T> section: sections) { boolean validHeader = headers.get(subHeader) != null; if(position == subPosition && validHeader) { - onBindHeaderHolder(holder, headers.get(subHeader)); + onBindHeaderHolder(holder, headers.get(subHeader), subHeader); return; } @@ -289,7 +289,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(new BasicHeaderView(context)); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); if(view != null) { view.setObject(header); @@ -414,14 +414,15 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { currentActionMode = mode; - onCreateActionModeMenu(menu, mode.getMenuInflater()); - MenuUtil.hideMenuItems(context, menu, updateView); T item = holder.getItem(); selected.add(item); selectedViews.add(updateView); setChecked(updateView, true); + onCreateActionModeMenu(menu, mode.getMenuInflater()); + MenuUtil.hideMenuItems(context, menu, updateView); + mode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { TypedValue typedValue = new TypedValue(); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java index 1cb9c34e..4e75a2f7 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java @@ -16,18 +16,20 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.User; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.UserUtil; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.RecyclingImageView; import github.daneren2005.dsub.view.SettingView; import github.daneren2005.dsub.view.UpdateView; @@ -37,56 +39,81 @@ import static github.daneren2005.dsub.domain.User.Setting; public class SettingsAdapter extends SectionAdapter<Setting> { private static final String TAG = SettingsAdapter.class.getSimpleName(); public final int VIEW_TYPE_SETTING = 1; + public final int VIEW_TYPE_SETTING_HEADER = 2; private final User user; private final boolean editable; private final ImageLoader imageLoader; - public SettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { - super(context, user.getSettings(), imageLoader != null); + public SettingsAdapter(Context context, User user, List<String> headers, List<List<User.Setting>> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { + super(context, headers, settingSections, imageLoader != null); this.user = user; this.imageLoader = imageLoader; this.editable = editable; this.onItemClickedListener = onItemClickedListener; - List<Setting> settings = sections.get(0); - for(Setting setting: settings) { - if(setting.getValue()) { - addSelected(setting); + for(List<Setting> settings: sections) { + for (Setting setting : settings) { + if (setting.getValue()) { + addSelected(setting); + } } } } + @Override + public int getItemViewType(int position) { + int viewType = super.getItemViewType(position); + if(viewType == SectionAdapter.VIEW_TYPE_HEADER) { + if(position == 0 && imageLoader != null) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_SETTING_HEADER; + } + } else { + return viewType; + } + } + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { View header = LayoutInflater.from(context).inflate(R.layout.user_header, parent, false); return new UpdateView.UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) { View header = holder.getView(); RecyclingImageView coverArtView = (RecyclingImageView) header.findViewById(R.id.user_avatar); - imageLoader.loadAvatar(context, coverArtView, user.getUsername()); - coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { - @Override - public void onInvalidated(RecyclingImageView imageView) { - imageLoader.loadAvatar(context, imageView, user.getUsername()); + if(coverArtView != null) { + imageLoader.loadAvatar(context, coverArtView, user.getUsername()); + coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { + @Override + public void onInvalidated(RecyclingImageView imageView) { + imageLoader.loadAvatar(context, imageView, user.getUsername()); + } + }); + + TextView usernameView = (TextView) header.findViewById(R.id.user_username); + usernameView.setText(user.getUsername()); + + final TextView emailView = (TextView) header.findViewById(R.id.user_email); + if (user.getEmail() != null) { + emailView.setText(user.getEmail()); + } else { + emailView.setVisibility(View.GONE); } - }); - - TextView usernameView = (TextView) header.findViewById(R.id.user_username); - usernameView.setText(user.getUsername()); - - final TextView emailView = (TextView) header.findViewById(R.id.user_email); - if(user.getEmail() != null) { - emailView.setText(user.getEmail()); } else { - emailView.setVisibility(View.GONE); + TextView nameView = (TextView) header.findViewById(R.id.item_name); + nameView.setText(description); } } @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new SettingView(context)); + if(viewType == VIEW_TYPE_SETTING_HEADER) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context)); + } else { + return new UpdateView.UpdateViewHolder(new SettingView(context)); + } } @Override @@ -105,4 +132,21 @@ public class SettingsAdapter extends SectionAdapter<Setting> { updateView.setChecked(checked); } } + + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener<Setting> onItemClickedListener) { + return getSettingsAdapter(context, user, imageLoader, UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), onItemClickedListener); + } + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener<Setting> onItemClickedListener) { + List<String> headers = new ArrayList<>(); + List<List<User.Setting>> settingsSections = new ArrayList<>(); + headers.add(context.getResources().getString(R.string.admin_permissions)); + settingsSections.add(user.getSettings()); + + if(user.getMusicFolderSettings() != null) { + headers.add(context.getResources().getString(R.string.admin_musicFolders)); + settingsSections.add(user.getMusicFolderSettings()); + } + + return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener); + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java new file mode 100644 index 00000000..2234d4cd --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java @@ -0,0 +1,53 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + Copyright 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.UpdateView; + +public class SimilarArtistAdapter extends SectionAdapter<Artist> { + public static int VIEW_TYPE_ARTIST = 4; + + public SimilarArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener onItemClickedListener) { + super(context, artists); + this.onItemClickedListener = onItemClickedListener; + } + public SimilarArtistAdapter(Context context, List<String> headers, List<List<Artist>> sections, OnItemClickedListener onItemClickedListener) { + super(context, headers, sections); + this.onItemClickedListener = onItemClickedListener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new ArtistView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(Artist item) { + return VIEW_TYPE_ARTIST; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java index 56e8f92e..ff4d86ce 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java @@ -21,15 +21,19 @@ package github.daneren2005.dsub.domain; import android.util.Log; import java.io.Serializable; +import java.text.Collator; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Locale; /** * @author Sindre Mehus */ public class Artist implements Serializable { private static final String TAG = Artist.class.getSimpleName(); + public static final String ROOT_ID = "-1"; + public static final String MISSING_ID = "-2"; private String id; private String name; @@ -38,6 +42,14 @@ public class Artist implements Serializable { private Integer rating; private int closeness; + public Artist() { + + } + public Artist(String id, String name) { + this.id = id; + this.name = name; + } + public String getId() { return id; } @@ -109,30 +121,18 @@ public class Artist implements Serializable { public static class ArtistComparator implements Comparator<Artist> { private String[] ignoredArticles; + private Collator collator; public ArtistComparator(String[] ignoredArticles) { this.ignoredArticles = ignoredArticles; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Artist lhsArtist, Artist rhsArtist) { - if("root".equals(lhsArtist.getId())) { - return 1; - } else if("root".equals(rhsArtist.getId())) { - return -1; - } - String lhs = lhsArtist.getName().toLowerCase(); String rhs = rhsArtist.getName().toLowerCase(); - char lhs1 = lhs.charAt(0); - char rhs1 = rhs.charAt(0); - - if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { - return 1; - } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { - return -1; - } - for (String article : ignoredArticles) { int index = lhs.indexOf(article.toLowerCase() + " "); if (index == 0) { @@ -144,7 +144,7 @@ public class Artist implements Serializable { } } - return lhs.compareTo(rhs); + return collator.compare(lhs, rhs); } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java index e15ccf9f..05e686ca 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java @@ -52,13 +52,6 @@ public class Indexes implements Serializable { this.shortcuts = shortcuts; this.artists = artists; this.entries = entries; - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } } public long getLastModified() { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java index 3c022cea..1da3d51b 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java @@ -24,6 +24,8 @@ import android.content.SharedPreferences; import android.media.MediaMetadataRetriever; import android.os.Build; import android.util.Log; + +import java.text.Collator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -31,8 +33,11 @@ import java.io.File; import java.io.Serializable; import java.util.Collections; import java.util.Comparator; +import java.util.Locale; +import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.Util; /** @@ -135,13 +140,14 @@ public class MusicDirectory implements Serializable { EntryComparator.sort(children, byYear); } - public synchronized void updateMetadata(MusicDirectory refreshedDirectory) { + public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) { + boolean metadataUpdated = false; Iterator<Entry> it = children.iterator(); while(it.hasNext()) { Entry entry = it.next(); int index = refreshedDirectory.children.indexOf(entry); if(index != -1) { - Entry refreshed = refreshedDirectory.children.get(index); + final Entry refreshed = refreshedDirectory.children.get(index); entry.setTitle(refreshed.getTitle()); entry.setAlbum(refreshed.getAlbum()); @@ -155,8 +161,36 @@ public class MusicDirectory implements Serializable { entry.setStarred(refreshed.isStarred()); entry.setRating(refreshed.getRating()); entry.setType(refreshed.getType()); + if(!Util.equals(entry.getCoverArt(), refreshed.getCoverArt())) { + metadataUpdated = true; + entry.setCoverArt(refreshed.getCoverArt()); + } + + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setTitle(refreshed.getTitle()); + found.setAlbum(refreshed.getAlbum()); + found.setArtist(refreshed.getArtist()); + found.setTrack(refreshed.getTrack()); + found.setYear(refreshed.getYear()); + found.setGenre(refreshed.getGenre()); + found.setTranscodedContentType(refreshed.getTranscodedContentType()); + found.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + found.setDiscNumber(refreshed.getDiscNumber()); + found.setStarred(refreshed.isStarred()); + found.setRating(refreshed.getRating()); + found.setType(refreshed.getType()); + if(!Util.equals(found.getCoverArt(), refreshed.getCoverArt())) { + found.setCoverArt(refreshed.getCoverArt()); + metadataUpdate = DownloadService.METADATA_UPDATED_COVER_ART; + } + } + }.execute(); } } + + return metadataUpdated; } public synchronized boolean updateEntriesList(Context context, int instance, MusicDirectory refreshedDirectory) { boolean changed = false; @@ -275,6 +309,10 @@ public class MusicDirectory implements Serializable { public void rebaseTitleOffPath() { try { String filename = getPath(); + if(filename == null) { + return; + } + int index = filename.lastIndexOf('/'); if (index != -1) { filename = filename.substring(index + 1); @@ -586,9 +624,12 @@ public class MusicDirectory implements Serializable { public static class EntryComparator implements Comparator<Entry> { private boolean byYear; + private Collator collator; public EntryComparator(boolean byYear) { this.byYear = byYear; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Entry lhs, Entry rhs) { @@ -608,8 +649,8 @@ public class MusicDirectory implements Serializable { return 1; } } - - return lhs.getAlbumDisplay().compareToIgnoreCase(rhs.getAlbumDisplay()); + + return collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay()); } Integer lhsDisc = lhs.getDiscNumber(); @@ -633,7 +674,7 @@ public class MusicDirectory implements Serializable { return 1; } - return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + return collator.compare(lhs.getTitle(), rhs.getTitle()); } public static void sort(List<Entry> entries) { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java index 99e86e23..37f76249 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java @@ -18,7 +18,12 @@ */ package github.daneren2005.dsub.domain; +import android.util.Log; + import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; /** * Represents a top level directory in which music or other media is stored. @@ -27,23 +32,49 @@ import java.io.Serializable; * @version $Id$ */ public class MusicFolder implements Serializable { - - private String id; - private String name; + private static final String TAG = MusicFolder.class.getSimpleName(); + private String id; + private String name; + private boolean enabled; public MusicFolder() { } - public MusicFolder(String id, String name) { - this.id = id; - this.name = name; - } + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean getEnabled() { + return enabled; + } - public String getId() { - return id; - } + public static class MusicFolderComparator implements Comparator<MusicFolder> { + public int compare(MusicFolder lhsMusicFolder, MusicFolder rhsMusicFolder) { + if(lhsMusicFolder == rhsMusicFolder || lhsMusicFolder.getName().equals(rhsMusicFolder.getName())) { + return 0; + } else { + return lhsMusicFolder.getName().compareToIgnoreCase(rhsMusicFolder.getName()); + } + } + } - public String getName() { - return name; - } + public static void sort(List<MusicFolder> musicFolders) { + try { + Collections.sort(musicFolders, new MusicFolderComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort music folders", e); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java index 20d46aa0..ed2400ef 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java @@ -18,6 +18,8 @@ */ package github.daneren2005.dsub.domain; +import java.util.regex.Pattern; + /** * The criteria for a music search. * @@ -25,31 +27,67 @@ package github.daneren2005.dsub.domain; */ public class SearchCritera { - private final String query; - private final int artistCount; - private final int albumCount; - private final int songCount; - - public SearchCritera(String query, int artistCount, int albumCount, int songCount) { - this.query = query; - this.artistCount = artistCount; - this.albumCount = albumCount; - this.songCount = songCount; - } - - public String getQuery() { - return query; - } - - public int getArtistCount() { - return artistCount; - } - - public int getAlbumCount() { - return albumCount; - } - - public int getSongCount() { - return songCount; - } -}
\ No newline at end of file + private final String query; + private final int artistCount; + private final int albumCount; + private final int songCount; + private Pattern pattern; + + public SearchCritera(String query, int artistCount, int albumCount, int songCount) { + this.query = query; + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + } + + public String getQuery() { + return query; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } + + /** + * Returns and caches a pattern instance that can be used to check if a + * string matches the query. + */ + public Pattern getPattern() { + + // If the pattern wasn't already cached, create a new regular expression + // from the search string : + // * Surround the search string with ".*" (match anything) + // * Replace spaces and wildcard '*' characters with ".*" + // * All other characters are properly quoted + if (this.pattern == null) { + String regex = ".*"; + String currentPart = ""; + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + if (c == '*' || c == ' ') { + regex += Pattern.quote(currentPart); + regex += ".*"; + currentPart = ""; + } else { + currentPart += c; + } + } + if (currentPart.length() > 0) { + regex += Pattern.quote(currentPart); + } + + regex += ".*"; + this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + return this.pattern; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java index 73037c4a..7f538484 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java @@ -35,6 +35,7 @@ import github.daneren2005.dsub.util.Util; public class ServerInfo implements Serializable { public static final int TYPE_SUBSONIC = 1; public static final int TYPE_MADSONIC = 2; + public static final int TYPE_AMPACHE = 3; private static final Map<Integer, ServerInfo> SERVERS = new ConcurrentHashMap<Integer, ServerInfo>(); private boolean isLicenseValid; @@ -189,13 +190,20 @@ public class ServerInfo implements Serializable { public static boolean isMadsonic6(Context context, int instance) { return getServerType(context, instance) == TYPE_MADSONIC && checkServerVersion(context, "2.0", instance); } + + public static boolean isAmpache(Context context) { + return isAmpache(context, Util.getActiveServer(context)); + } + public static boolean isAmpache(Context context, int instance) { + return getServerType(context, instance) == TYPE_AMPACHE; + } private static String getCacheName(Context context, int instance) { return "server-" + Util.getRestUrl(context, null, instance, false).hashCode() + ".ser"; } public static boolean hasArtistInfo(Context context) { - if(isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { + if(!isMadsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { return true; } else if(isMadsonic(context)) { return checkServerVersion(context, "2.0"); diff --git a/app/src/main/java/github/daneren2005/dsub/domain/User.java b/app/src/main/java/github/daneren2005/dsub/domain/User.java index 797a1271..5307828a 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/User.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/User.java @@ -15,6 +15,8 @@ package github.daneren2005.dsub.domain; +import android.util.Pair; + import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -31,8 +33,9 @@ public class User implements Serializable { public static final String STREAM = "streamRole"; public static final String JUKEBOX = "jukeboxRole"; public static final String SHARE = "shareRole"; + public static final String VIDEO_CONVERSION = "videoConversionRole"; public static final String LASTFM = "lastFMRole"; - public static final List<String> ROLES = new ArrayList<String>(); + public static final List<String> ROLES = new ArrayList<>(); static { ROLES.add(ADMIN); @@ -45,6 +48,7 @@ public class User implements Serializable { ROLES.add(PODCAST); ROLES.add(JUKEBOX); ROLES.add(SHARE); + ROLES.add(VIDEO_CONVERSION); } private String username; @@ -52,6 +56,7 @@ public class User implements Serializable { private String email; private List<Setting> settings = new ArrayList<Setting>(); + private List<Setting> musicFolders; public User() { @@ -92,9 +97,27 @@ public class User implements Serializable { settings.add(new Setting(name, value)); } + public void addMusicFolder(MusicFolder musicFolder) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolder.getId(), musicFolder.getName(), false)); + } + public void addMusicFolder(MusicFolderSetting musicFolderSetting, boolean defaultValue) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolderSetting.getName(), musicFolderSetting.getLabel(), defaultValue)); + } + public List<Setting> getMusicFolderSettings() { + return musicFolders; + } + public static class Setting implements Serializable { - String name; - Boolean value; + private String name; + private Boolean value; public Setting() { @@ -114,4 +137,20 @@ public class User implements Serializable { this.value = value; } } + + public static class MusicFolderSetting extends Setting { + private String label; + + public MusicFolderSetting() { + + } + public MusicFolderSetting(String name, String label, Boolean value) { + super(name, value); + this.label = label; + } + + public String getLabel() { + return label; + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Version.java b/app/src/main/java/github/daneren2005/dsub/domain/Version.java index 97246ecf..9df0dbb4 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Version.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Version.java @@ -27,41 +27,41 @@ import java.io.Serializable; * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ */ public class Version implements Comparable<Version>, Serializable { - private int major; - private int minor; - private int beta; - private int bugfix; + private int major; + private int minor; + private int beta; + private int bugfix; public Version() { // For Kryo } - /** - * Creates a new version instance by parsing the given string. - * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". - */ - public Version(String version) { - String[] s = version.split("\\."); - major = Integer.valueOf(s[0]); - minor = Integer.valueOf(s[1]); - - if (s.length > 2) { - if (s[2].contains("beta")) { - beta = Integer.valueOf(s[2].replace("beta", "")); - } else { - bugfix = Integer.valueOf(s[2]); - } - } - } - - public int getMajor() { - return major; - } - - public int getMinor() { - return minor; - } - + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + public String getVersion() { switch(major) { case 1: @@ -90,96 +90,98 @@ public class Version implements Comparable<Version>, Serializable { return "4.9"; case 11: return "5.1"; - case 12: - return "5.2"; - case 13: - return "5.3"; + case 12: + return "5.2"; + case 13: + return "5.3"; + case 14: + return "6.0"; } } return ""; } - /** - * Return whether this object is equal to another. - * @param o Object to compare to. - * @return Whether this object is equals to another. - */ - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final Version version = (Version) o; - - if (beta != version.beta) return false; - if (bugfix != version.bugfix) return false; - if (major != version.major) return false; - return minor == version.minor; - } - - /** - * Returns a hash code for this object. - * @return A hash code for this object. - */ - public int hashCode() { - int result; - result = major; - result = 29 * result + minor; - result = 29 * result + beta; - result = 29 * result + bugfix; - return result; - } - - /** - * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". - * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". - */ - public String toString() { - StringBuffer buf = new StringBuffer(); - buf.append(major).append('.').append(minor); - if (beta != 0) { - buf.append(".beta").append(beta); - } else if (bugfix != 0) { - buf.append('.').append(bugfix); - } - - return buf.toString(); - } - - /** - * Compares this object with the specified object for order. - * @param version The object to compare to. - * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or - * greater than the specified object. - */ - @Override - public int compareTo(Version version) { - if (major < version.major) { - return -1; - } else if (major > version.major) { - return 1; - } - - if (minor < version.minor) { - return -1; - } else if (minor > version.minor) { - return 1; - } - - if (bugfix < version.bugfix) { - return -1; - } else if (bugfix > version.bugfix) { - return 1; - } - - int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; - int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; - - if (thisBeta < otherBeta) { - return -1; - } else if (thisBeta > otherBeta) { - return 1; - } - - return 0; - } + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + @Override + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java index 630acf2c..187f0d55 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java @@ -47,7 +47,7 @@ public class AdminFragment extends SelectRecyclerFragment<User> { switch (item.getItemId()) { case R.id.menu_add_user: - UserUtil.addNewUser(context, this); + UserUtil.addNewUser(context, this, objects.get(0)); break; } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java index 5daf3d7a..8c2fa4bf 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java @@ -1,11 +1,12 @@ package github.daneren2005.dsub.fragments; -import android.content.res.Resources; -import android.os.Environment; import android.content.Intent; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.StatFs; import android.util.Log; @@ -18,6 +19,7 @@ import github.daneren2005.dsub.adapter.MainAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.EnvironmentVariables; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.ProgressListener; @@ -28,13 +30,28 @@ import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.view.ChangeLog; import github.daneren2005.dsub.view.UpdateView; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.net.ssl.HttpsURLConnection; + public class MainFragment extends SelectRecyclerFragment<Integer> { private static final String TAG = MainFragment.class.getSimpleName(); + public static final String SONGS_LIST_PREFIX = "songs-"; + public static final String SONGS_NEWEST = SONGS_LIST_PREFIX + "newest"; + public static final String SONGS_TOP_PLAYED = SONGS_LIST_PREFIX + "topPlayed"; + public static final String SONGS_RECENT = SONGS_LIST_PREFIX + "recent"; + public static final String SONGS_FREQUENT = SONGS_LIST_PREFIX + "frequent"; public MainFragment() { super(); @@ -47,6 +64,7 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.main, menu); + onFinishSetupOptionsMenu(menu); try { if (!ServerInfo.isMadsonic(context) || !UserUtil.isCurrentAdmin()) { @@ -113,6 +131,22 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { sections.add(albums); headers.add("albums"); + if(ServerInfo.isMadsonic6(context)) { + List<Integer> songs = new ArrayList<>(); + + songs.add(R.string.main_songs_newest); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_top_played); + } + songs.add(R.string.main_songs_recent); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_frequent); + } + + sections.add(songs); + headers.add("songs"); + } + if(ServerInfo.checkServerVersion(context, "1.8")) { List<Integer> videos = Arrays.asList(R.string.main_videos); sections.add(videos); @@ -237,10 +271,10 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { private void getLogs() { try { - final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - new LoadingTask<File>(context) { + final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + new LoadingTask<String>(context) { @Override - protected File doInBackground() throws Throwable { + protected String doInBackground() throws Throwable { updateProgress("Gathering Logs"); File logcat = new File(Environment.getExternalStorageDirectory(), "dsub-logcat.txt"); Util.delete(logcat); @@ -258,30 +292,94 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()])); logcatProc.waitFor(); - } catch(Exception e) { - Util.toast(context, "Failed to gather logs"); } finally { if(logcatProc != null) { logcatProc.destroy(); } } - return logcat; + URL url = new URL("https://pastebin.com/api/api_post.php"); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + StringBuffer responseBuffer = new StringBuffer(); + try { + urlConnection.setReadTimeout(10000); + urlConnection.setConnectTimeout(15000); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + + OutputStream os = urlConnection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, Constants.UTF_8)); + writer.write("api_dev_key=" + URLEncoder.encode(EnvironmentVariables.PASTEBIN_DEV_KEY, Constants.UTF_8) + "&api_option=paste&api_paste_private=1&api_paste_code="); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logcat))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + + File stacktrace = new File(Environment.getExternalStorageDirectory(), "dsub-stacktrace.txt"); + if(stacktrace.exists() && stacktrace.isFile()) { + writer.write("\n\nMost Recent Stacktrace:\n\n"); + + reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(stacktrace))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + } + + writer.flush(); + writer.close(); + os.close(); + + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseBuffer.append(inputLine); + } + in.close(); + } finally { + urlConnection.disconnect(); + } + + String response = responseBuffer.toString(); + if(response.indexOf("http") == 0) { + return response.replace("http:", "https:"); + } else { + throw new Exception("Pastebin Error: " + response); + } + } + + @Override + protected void error(Throwable error) { + Log.e(TAG, "Failed to gather logs", error); + Util.toast(context, "Failed to gather logs"); } @Override - protected void done(File logcat) { + protected void done(String logcat) { String footer = "Android SDK: " + Build.VERSION.SDK; footer += "\nDevice Model: " + Build.MODEL; footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT; footer += "\nROM: " + Build.DISPLAY; + footer += "\nLogs: " + logcat; + footer += "\nBuild Number: " + packageInfo.versionCode; Intent email = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "dsub.android@gmail.com", null)); - email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs"); + email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + packageInfo.versionName + " Error Logs"); email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer); - Uri attachment = Uri.fromFile(logcat); - email.putExtra(Intent.EXTRA_STREAM, attachment); startActivity(email); } }.execute(); @@ -310,6 +408,14 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { showAlbumList("alphabeticalByName"); } else if(item == R.string.main_videos) { showVideos(); + } else if (item == R.string.main_songs_newest) { + showAlbumList(SONGS_NEWEST); + } else if (item == R.string.main_songs_top_played) { + showAlbumList(SONGS_TOP_PLAYED); + } else if (item == R.string.main_songs_recent) { + showAlbumList(SONGS_RECENT); + } else if (item == R.string.main_songs_frequent) { + showAlbumList(SONGS_FREQUENT); } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java index c557a174..8a6df2ab 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -88,7 +88,6 @@ import java.util.concurrent.ScheduledFuture; public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener<DownloadFile>, OnSongChangedListener { private static final String TAG = NowPlayingFragment.class.getSimpleName(); private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; - private static final int INCREMENT_TIME = 5000; private static final int ACTION_PREVIOUS = 1; private static final int ACTION_NEXT = 2; @@ -106,6 +105,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private SeekBar progressBar; private AutoRepeatButton previousButton; private AutoRepeatButton nextButton; + private AutoRepeatButton rewindButton; + private AutoRepeatButton fastforwardButton; private View pauseButton; private View stopButton; private View startButton; @@ -172,6 +173,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar); previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous); nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next); + rewindButton = (AutoRepeatButton) rootView.findViewById(R.id.download_rewind); + fastforwardButton = (AutoRepeatButton) rootView.findViewById(R.id.download_fastforward); pauseButton =rootView.findViewById(R.id.download_pause); stopButton =rootView.findViewById(R.id.download_stop); startButton =rootView.findViewById(R.id.download_start); @@ -194,6 +197,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View v) { getDownloadService().toggleStarred(); + setControlsVisible(true); } }); } else { @@ -239,7 +243,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); previousButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(-INCREMENT_TIME); + changeProgress(true); } }); @@ -259,10 +263,35 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); nextButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(INCREMENT_TIME); + changeProgress(false); + } + }); + + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(true); + } + }); + rewindButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(false); + } + }); + fastforwardButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); } }); + pauseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -330,6 +359,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View view) { createBookmark(); + setControlsVisible(true); } }); @@ -341,6 +371,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(1); + setControlsVisible(true); } }); rateGoodButton.setOnClickListener(new View.OnClickListener() { @@ -351,6 +382,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(5); + setControlsVisible(true); } }); @@ -405,11 +437,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } }); - if(Build.MODEL.equals("Nexus 4") || Build.MODEL.equals("GT-I9100")) { - View slider = rootView.findViewById(R.id.download_slider); - slider.setPadding(0, 0, 0, 0); - } - return rootView; } @@ -457,6 +484,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector()); } } + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) { + menu.findItem(R.id.menu_batch_mode).setChecked(true); + } } @Override @@ -474,7 +505,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis menuInflater.inflate(R.menu.nowplaying_context_offline, menu); } else { menuInflater.inflate(R.menu.nowplaying_context, menu); - menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); + menu.findItem(R.id.song_menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); } if (downloadFile.getSong().getParent() == null) { @@ -620,9 +651,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } createNewPlaylist(entries, true); return true; - case R.id.menu_star: - UpdateHelper.toggleStarred(context, song.getSong()); - return true; case R.id.menu_rate: UpdateHelper.setRating(context, song.getSong()); return true; @@ -634,11 +662,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis startTimer(); } return true; - case R.id.menu_add_playlist: - songs = new ArrayList<Entry>(1); - songs.add(song.getSong()); - addToPlaylist(songs); - return true; case R.id.menu_info: displaySongInfo(song.getSong()); return true; @@ -663,7 +686,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis // Any failed condition will get here Util.toast(context, "Failed to start equalizer. Try restarting."); return true; - } default: + }case R.id.menu_batch_mode: + if(Util.isBatchMode(context)) { + Util.setBatchMode(context, false); + songListAdapter.notifyDataSetChanged(); + } else { + Util.setBatchMode(context, true); + songListAdapter.notifyDataSetChanged(); + } + context.supportInvalidateOptionsMenu(); + + return true; + default: return false; } } @@ -678,13 +712,13 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } private void onResumeHandlers() { - final Handler handler = new Handler(); executorService = Executors.newSingleThreadScheduledExecutor(); setControlsVisible(true); final DownloadService downloadService = getDownloadService(); if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) { playlistFlipper.setDisplayedChild(1); + startFlipped = false; } if (downloadService != null && downloadService.getKeepScreenOn()) { context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -701,7 +735,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis context.runWhenServiceAvailable(new Runnable() { @Override public void run() { - if(primaryFragment) { + if (primaryFragment) { DownloadService downloadService = getDownloadService(); downloadService.startRemoteScan(); downloadService.addOnSongChangedListener(NowPlayingFragment.this, true); @@ -899,10 +933,14 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private int getMinutes(int progress) { if(progress < 30) { return progress + 1; - } else if(progress < 61) { + } else if(progress < 49) { return (progress - 30) * 5 + getMinutes(29); + } else if(progress < 57) { + return (progress - 48) * 30 + getMinutes(48); + } else if(progress < 81) { + return (progress - 56) * 60 + getMinutes(56); } else { - return (progress - 61) * 15 + getMinutes(60); + return (progress - 80) * 150 + getMinutes(80); } } @@ -938,31 +976,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } - private void changeProgress(final int ms) { + private void changeProgress(final boolean rewind) { final DownloadService downloadService = getDownloadService(); if(downloadService == null) { return; } new SilentBackgroundTask<Void>(context) { - boolean isJukeboxEnabled; - int msPlayed; - Integer duration; - PlayerState playerState; int seekTo; @Override protected Void doInBackground() throws Throwable { - msPlayed = Math.max(0, downloadService.getPlayerPosition()); - duration = downloadService.getPlayerDuration(); - playerState = getDownloadService().getPlayerState(); - int msTotal = duration == null ? 0 : duration; - if(msPlayed + ms > msTotal) { - seekTo = msTotal; + if(rewind) { + seekTo = downloadService.rewind(); } else { - seekTo = msPlayed + ms; + seekTo = downloadService.fastForward(); } - downloadService.seekTo(seekTo); return null; } @@ -1158,11 +1187,37 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + + if(currentPlaying != null && !currentPlaying.isSong()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + } + + private void setupSubtitle(int currentPlayingIndex) { if (currentPlaying != null) { Entry song = currentPlaying.getSong(); songTitleTextView.setText(song.getTitle()); getImageLoader().loadImage(albumArtImageView, song, true, true); - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle)); + } else if(downloadService.isArtistRadio()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_artist_radio)); + } else { + setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + } } else { songTitleTextView.setText(null); getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); @@ -1199,10 +1254,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis scrollWhenLoaded = false; } - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); if(this.currentPlaying != currentPlaying) { onSongChanged(currentPlaying, currentPlayingIndex); onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + } else { + setupSubtitle(currentPlayingIndex); } } @@ -1334,6 +1390,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } bookmarkButton.setImageResource(bookmark); } + + if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + getImageLoader().loadImage(albumArtImageView, song, true, true); + } } public void updateRepeatButton() { @@ -1352,4 +1412,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis break; } } + + @Override + protected List<Entry> getSelectedEntries() { + List<DownloadFile> selected = getCurrentAdapter().getSelected(); + List<Entry> entries = new ArrayList<>(); + + for(DownloadFile downloadFile: selected) { + if(downloadFile.getSong() != null) { + entries.add(downloadFile.getSong()); + } + } + + return entries; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java index d21b82e0..eed714af 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java @@ -7,6 +7,7 @@ import java.util.List; import android.content.Intent; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; @@ -39,9 +40,9 @@ import github.daneren2005.dsub.view.UpdateView; public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener<Serializable> { private static final String TAG = SearchFragment.class.getSimpleName(); - private static final int MAX_ARTISTS = 10; - private static final int MAX_ALBUMS = 10; - private static final int MAX_SONGS = 25; + private static final int MAX_ARTISTS = 20; + private static final int MAX_ALBUMS = 20; + private static final int MAX_SONGS = 50; private static final int MIN_CLOSENESS = 1; protected RecyclerView recyclerView; @@ -108,13 +109,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { int viewType = adapter.getItemViewType(position); if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == ArtistAdapter.VIEW_TYPE_ARTIST) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -125,18 +126,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.search, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_search: - context.startSearch(currentQuery, false, null, false); - return true; - } - - return super.onOptionsItemSelected(item); - + onFinishSetupOptionsMenu(menu); } @Override @@ -187,6 +177,11 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O return selectedMedia; } + @Override + protected boolean isShowArtistEnabled() { + return true; + } + public void search(final String query, final boolean autoplay) { if(skipSearch) { skipSearch = false; @@ -213,6 +208,14 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } }; task.execute(); + + if(searchItem != null) { + MenuItemCompat.collapseActionView(searchItem); + } + } + + protected String getCurrentQuery() { + return currentQuery; } private void onArtistSelected(Artist artist, boolean autoplay) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java index 3df0a9a9..c9b94c9a 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java @@ -17,6 +17,7 @@ import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.Indexes; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.service.MusicService; @@ -29,12 +30,11 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -public class SelectArtistFragment extends SelectRecyclerFragment<Artist> implements ArtistAdapter.OnMusicFolderChanged { +public class SelectArtistFragment extends SelectRecyclerFragment<Serializable> implements ArtistAdapter.OnMusicFolderChanged { private static final String TAG = SelectArtistFragment.class.getSimpleName(); - private static final int MENU_GROUP_MUSIC_FOLDER = 10; private List<MusicFolder> musicFolders = null; - private List<MusicDirectory.Entry> entries; + private List<Entry> entries; private String groupId; private String groupName; @@ -63,12 +63,14 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { Bundle args = getArguments(); if(args != null) { - groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); - groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + if(args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)) { + groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); + groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); - if(groupName != null) { - setTitle(groupName); - context.invalidateOptionsMenu(); + if (groupName != null) { + setTitle(groupName); + context.invalidateOptionsMenu(); + } } } @@ -78,47 +80,68 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) { onCreateContextMenuSupport(menu, menuInflater, updateView, item); recreateContextMenu(menu); } @Override - public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Artist> updateView, Artist item) { + public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Serializable> updateView, Serializable item) { return onContextItemSelected(menuItem, item); } @Override - public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { + public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) { SubsonicFragment fragment; - if((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || "root".equals(artist.getId()) || groupId != null) { - fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - - if ("root".equals(artist.getId())) { - args.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries); - } - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + if(item instanceof Artist) { + Artist artist = (Artist) item; + + if ((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || groupId != null) { + fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + + fragment.setArguments(args); + } else { + fragment = new SelectArtistFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + + fragment.setArguments(args); } - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); + replaceFragment(fragment); } else { - fragment = new SelectArtistFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + Entry entry = (Entry) item; + if (entry.isVideo()) { + playVideo(entry); + } else { + List<Entry> songs = new ArrayList<Entry>(); + + if (Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true)) { + for (Entry song : entries) { + if (!song.isDirectory() && !song.isVideo()) { + songs.add(song); + } + } + playNow(songs, entry, 0); + } else { + songs.add(entry); + playNow(songs); + } } - - fragment.setArguments(args); } - - replaceFragment(fragment); } @Override @@ -155,15 +178,15 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public SectionAdapter getAdapter(List<Artist> objects) { + public SectionAdapter getAdapter(List<Serializable> objects) { return new ArtistAdapter(context, objects, musicFolders, this, this); } @Override - public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { - List<Artist> artists; + public List<Serializable> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List<Serializable> items; if(groupId == null) { - if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) { + if (!Util.isOffline(context) && (!Util.isTagBrowsing(context) || ServerInfo.checkServerVersion(context, "1.14"))) { musicFolders = musicService.getMusicFolders(refresh, context, listener); // Hide folders option if there is only one @@ -178,14 +201,16 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener); indexes.sortChildren(context); - artists = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); - artists.addAll(indexes.getShortcuts()); - artists.addAll(indexes.getArtists()); + items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); + items.addAll(indexes.getShortcuts()); + items.addAll(indexes.getArtists()); entries = indexes.getEntries(); + items.addAll(entries); } else { - artists = new ArrayList<>(); + List<Artist> artists = new ArrayList<>(); + items = new ArrayList<>(); MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener); - for(MusicDirectory.Entry entry: dir.getChildren(true, false)) { + for(Entry entry: dir.getChildren(true, false)) { Artist artist = new Artist(); artist.setId(entry.getId()); artist.setName(entry.getTitle()); @@ -193,21 +218,17 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme artists.add(artist); } - entries = new ArrayList<>(); - entries.addAll(dir.getChildren(false, true)); - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } - - Indexes indexes = new Indexes(0, artists, new ArrayList<Artist>()); + Indexes indexes = new Indexes(0, new ArrayList<Artist>(), artists); indexes.sortChildren(context); + items.addAll(indexes.getArtists()); + + entries = dir.getChildren(false, true); + for(Entry entry: entries) { + items.add(entry); + } } - return artists; + return items; } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java index 5f3ca38b..03c6663a 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java @@ -18,6 +18,7 @@ */ package github.daneren2005.dsub.fragments; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -28,6 +29,7 @@ import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.MenuUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -89,19 +91,28 @@ public class SelectBookmarkFragment extends SelectRecyclerFragment<MusicDirector return; } - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - downloadService.clear(); - downloadService.download(Arrays.asList(bookmark), false, true, false, false, 0, bookmark.getBookmark().getPosition()); - return null; - } - - @Override - protected void done(Void result) { - context.openNowPlaying(); - } - }.execute(); + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true) && ((!Util.isTagBrowsing(context) && bookmark.getParent() != null) || (Util.isTagBrowsing(context) && bookmark.getAlbumId() != null)) && !bookmark.isPodcast()) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSiblingsRecursively(bookmark); + + if(songs.isEmpty() || !songs.contains(bookmark)) { + playNowInTask(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition()); + } else { + playNowInTask(songs, bookmark, bookmark.getBookmark().getPosition()); + } + return null; + } + + @Override + protected void done(Boolean result) { + context.openNowPlaying(); + } + }.execute(); + } else { + playNow(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition()); + } } private void displayBookmarkInfo(final MusicDirectory.Entry entry) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java index d2282117..de74d0ed 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -39,6 +39,7 @@ import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.Share; +import github.daneren2005.dsub.service.CachedMusicService; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; @@ -86,6 +87,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section private ArtistInfo artistInfo; private String artistInfoDelayed; + private SilentBackgroundTask updateCoverArtTask; + private ImageView coverArtView; + private Entry coverArtRep; + private String coverArtId; + String id; String name; Entry directory; @@ -184,27 +190,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section recyclerView.setHasFixedSize(true); fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); setupScrollList(recyclerView); - - if(largeAlbums) { - GridLayoutManager gridLayoutManager = new GridLayoutManager(context, getRecyclerColumnCount()); - gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - int viewType = entryGridAdapter.getItemViewType(position); - if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { - return getRecyclerColumnCount(); - } else { - return 1; - } - } - }); - recyclerView.addItemDecoration(new GridSpacingDecoration()); - recyclerView.setLayoutManager(gridLayoutManager); - } else { - LinearLayoutManager layoutManager = new LinearLayoutManager(context); - layoutManager.setOrientation(LinearLayoutManager.VERTICAL); - recyclerView.setLayoutManager(layoutManager); - } + setupLayoutManager(recyclerView, largeAlbums); if(entries == null) { if(primaryFragment || secondaryFragment) { @@ -248,7 +234,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!ServerInfo.hasTopSongs(context)) { menu.removeItem(R.id.menu_top_tracks); } - if(!ServerInfo.checkServerVersion(context, "1.11") || (id != null && "root".equals(id))) { + if(!ServerInfo.checkServerVersion(context, "1.11")) { menu.removeItem(R.id.menu_radio); menu.removeItem(R.id.menu_similar_artists); } else if(!ServerInfo.hasSimilarArtists(context)) { @@ -305,9 +291,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section case R.id.menu_show_all: setShowAll(); return true; - case R.id.menu_unstar: - unstarSelected(); - return true; case R.id.menu_top_tracks: showTopTracks(); return true; @@ -329,10 +312,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) { menu.removeItem(R.id.song_menu_remove_playlist); } - // Remove show artists if parent is not set and if not on a album list - if((albumListType == null || (entry.getParent() == null && entry.getArtistId() == null)) && !Util.isOffline(context)) { - menu.removeItem(R.id.album_menu_show_artist); - } recreateContextMenu(menu); } @@ -384,24 +363,28 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } else { List<Entry> songs = new ArrayList<Entry>(); - if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true)) { - Iterator it = entries.listIterator(entries.indexOf(entry)); - while(it.hasNext()) { - songs.add((Entry) it.next()); + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true) && (albumListType == null || "starred".equals(albumListType))) { + for(Entry song: entries) { + if(!song.isDirectory() && !song.isVideo()) { + songs.add(song); + } } + playNow(songs, entry, 0); } else { songs.add(entry); + playNow(songs); } - - playNow(songs); } } @Override protected void refresh(boolean refresh) { - if(!"root".equals(id)) { - load(refresh); - } + load(refresh); + } + + @Override + protected boolean isShowArtistEnabled() { + return albumListType != null; } private void load(boolean refresh) { @@ -577,6 +560,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section setTitle(albumListExtra); } else if("alphabeticalByName".equals(albumListType)) { setTitle(R.string.main_albums_alphabetical); + } if (MainFragment.SONGS_NEWEST.equals(albumListType)) { + setTitle(R.string.main_songs_newest); + } else if (MainFragment.SONGS_TOP_PLAYED.equals(albumListType)) { + setTitle(R.string.main_songs_top_played); + } else if (MainFragment.SONGS_RECENT.equals(albumListType)) { + setTitle(R.string.main_songs_recent); + } else if (MainFragment.SONGS_FREQUENT.equals(albumListType)) { + setTitle(R.string.main_songs_frequent); } new LoadTask(true) { @@ -593,6 +584,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } } else if("genres".equals(albumListType) || "genres-songs".equals(albumListType)) { result = service.getSongsByGenre(albumListExtra, size, 0, context, this); + } else if(albumListType.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(albumListType, size, 0, context, this); } else { result = service.getAlbumList(albumListType, size, 0, refresh, context, this); } @@ -657,9 +650,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } @Override - public void updateCache() { - if(entryGridAdapter != null) { + public void updateCache(int changeCode) { + if(entryGridAdapter != null && changeCode == CachedMusicService.CACHE_UPDATE_LIST) { entryGridAdapter.notifyDataSetChanged(); + } else if(changeCode == CachedMusicService.CACHE_UPDATE_METADATA) { + if(coverArtView != null && coverArtRep != null && !Util.equals(coverArtRep.getCoverArt(), coverArtId)) { + synchronized (coverArtRep) { + if (updateCoverArtTask != null && updateCoverArtTask.isRunning()) { + updateCoverArtTask.cancel(); + } + updateCoverArtTask = getImageLoader().loadImage(coverArtView, coverArtRep, false, true); + coverArtId = coverArtRep.getCoverArt(); + } + } } } } @@ -669,6 +672,21 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section return entryGridAdapter; } + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = entryGridAdapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + private void finishLoading() { boolean validData = !entries.isEmpty() || !albums.isEmpty(); if(!validData) { @@ -682,7 +700,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(albumListType == null || "starred".equals(albumListType)) { entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums); entryGridAdapter.setRemoveFromPlaylist(playlistId != null); - entryGridAdapter.setRemoveStarred(albumListType == null); } else { if("alphabeticalByName".equals(albumListType)) { entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums); @@ -728,11 +745,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!artist) { entryGridAdapter.setShowArtist(true); } + if(topTracks) { + entryGridAdapter.setShowAlbum(true); + } // Show header if not album list type and not root and not artist // For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists boolean addedHeader = false; - if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { + if(albumListType == null && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { View header = createHeader(); if(header != null) { @@ -953,70 +973,28 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }.execute(); } - public void unstarSelected() { - List<Entry> selected = getSelectedEntries(); - if(selected.size() == 0) { - selected = entries; - } - if(selected.size() == 0) { - return; - } - final List<Entry> unstar = new ArrayList<Entry>(); - unstar.addAll(selected); + @Override + protected void toggleSelectedStarred() { + UpdateHelper.OnStarChange onStarChange = null; + if(albumListType != null && "starred".equals(albumListType)) { + onStarChange = new UpdateHelper.OnStarChange() { + @Override + public void starChange(boolean starred) { - new LoadingTask<Void>(context, true) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - List<Entry> entries = new ArrayList<Entry>(); - List<Entry> artists = new ArrayList<Entry>(); - List<Entry> albums = new ArrayList<Entry>(); - for(Entry entry: unstar) { - if(entry.isDirectory() && Util.isTagBrowsing(context)) { - if(entry.isAlbum()) { - albums.add(entry); - } else { - artists.add(entry); - } - } else { - entries.add(entry); - } } - musicService.setStarred(entries, artists, albums, false, this, context); - for(Entry entry: unstar) { - new UpdateHelper.EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(false); + @Override + public void starCommited(boolean starred) { + if(!starred) { + for (Entry entry : entries) { + entryGridAdapter.removeItem(entry); } - }.execute(); - } - - return null; - } - - @Override - protected void done(Void result) { - Util.toast(context, context.getResources().getString(R.string.starring_content_unstarred, Integer.toString(unstar.size()))); - - for(Entry entry: unstar) { - entryGridAdapter.removeItem(entry); - } - } - - @Override - protected void error(Throwable error) { - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(R.string.starring_content_error, Integer.toString(unstar.size())) + " " + getErrorMessage(error); + } } + }; + } - Util.toast(context, msg, false); - } - }.execute(); + UpdateHelper.toggleStarred(context, getSelectedEntries(), onStarChange); } private void checkLicenseAndTrialPeriod(LoadingTask onValid) { @@ -1105,11 +1083,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section @Override protected Void doInBackground() throws Throwable { DownloadService downloadService = getDownloadService(); + downloadService.clear(); downloadService.setArtistRadio(artistId); - if(downloadService.size() == 0) { - Log.e(TAG, "Failed to create artist radio"); - throw new Exception("Failed to create artist radio"); - } return null; } @@ -1159,22 +1134,22 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }); imageLoader.loadImage(coverArtView, url, false); } else if(entries.size() > 0) { - Entry coverArt = null; - for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) { - coverArt = entries.get(random.nextInt(entries.size())); + coverArtRep = null; + this.coverArtView = coverArtView; + for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) { + coverArtRep = entries.get(random.nextInt(entries.size())); } - final Entry albumRep = coverArt; coverArtView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (albumRep.getCoverArt() == null) { + if (coverArtRep == null || coverArtRep.getCoverArt() == null) { return; } AlertDialog.Builder builder = new AlertDialog.Builder(context); ImageView fullScreenView = new ImageView(context); - imageLoader.loadImage(fullScreenView, albumRep, true, true); + imageLoader.loadImage(fullScreenView, coverArtRep, true, true); builder.setCancelable(true); AlertDialog imageDialog = builder.create(); @@ -1183,7 +1158,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section imageDialog.show(); } }); - imageLoader.loadImage(coverArtView, albumRep, false, true); + synchronized (coverArtRep) { + coverArtId = coverArtRep.getCoverArt(); + updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true); + } } coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { @@ -1349,6 +1327,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); } } + + @Override + public void starCommited(boolean starred) { + + } }); } }); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java index 6e2c9da5..28cf9911 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -184,6 +184,22 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { replaceFragment(fragment); } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String playlistId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (playlistId != null && objects != null) { + for (Playlist playlist : objects) { + if (playlistId.equals(playlist.getId())) { + onItemClicked(null, playlist); + break; + } + } + } + } + } + private void deletePlaylist(final Playlist playlist) { Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java index 3f8f7844..2e2a16b3 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -154,25 +154,18 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(newestEpisodes == null || newestEpisodes.getChildrenSize() == 0) { return new PodcastChannelAdapter(context, channels, hasCoverArt ? getImageLoader() : null, this, largeAlbums); } else { - List<String> headers = Arrays.asList(PodcastChannelAdapter.EPISODE_HEADER, PodcastChannelAdapter.CHANNEL_HEADER); + Resources res = context.getResources(); + List<String> headers = Arrays.asList(res.getString(R.string.main_albums_newest), res.getString(R.string.select_podcasts_channels)); List<MusicDirectory.Entry> episodes = newestEpisodes.getChildren(false, true); List<Serializable> serializableEpisodes = new ArrayList<>(); - - // Put 3 in current list - while(serializableEpisodes.size() < 3 && !episodes.isEmpty()) { - serializableEpisodes.add(episodes.remove(0)); - } - - // Put rest in extra set - List<Serializable> extraEpisodes = new ArrayList<>(); - extraEpisodes.addAll(episodes); + serializableEpisodes.addAll(episodes); List<List<Serializable>> sections = new ArrayList<>(); sections.add(serializableEpisodes); sections.add(channels); - return new PodcastChannelAdapter(context, headers, sections, extraEpisodes, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); + return new PodcastChannelAdapter(context, headers, sections, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); } } @@ -182,7 +175,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(!Util.isOffline(context) && ServerInfo.hasNewestPodcastEpisodes(context)) { try { - newestEpisodes = musicService.getNewestPodcastEpisodes(10, context, listener); + newestEpisodes = musicService.getNewestPodcastEpisodes(refresh, context, listener, 10); for(MusicDirectory.Entry entry: newestEpisodes.getChildren()) { for(PodcastChannel channel: channels) { @@ -251,7 +244,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { @@ -259,7 +252,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(adapter != null) { int viewType = getCurrentAdapter().getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_EPISODE || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_LEGACY) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -270,6 +263,25 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> }; } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String podcastId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (podcastId != null && objects != null) { + for (Serializable ser : objects) { + if (ser instanceof PodcastChannel) { + PodcastChannel podcast = (PodcastChannel) ser; + if (podcastId.equals(podcast.getId())) { + onItemClicked(null, podcast); + break; + } + } + } + } + } + } + public void refreshPodcasts() { new SilentBackgroundTask<Void>(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java index 7ae7fff8..0d4506ac 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java @@ -15,10 +15,15 @@ package github.daneren2005.dsub.fragments; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -102,6 +107,7 @@ public abstract class SelectRecyclerFragment<T> extends SubsonicFragment impleme } menuInflater.inflate(getOptionsMenu(), menu); + onFinishSetupOptionsMenu(menu); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java index cb0e48b9..f231fa33 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java @@ -27,6 +27,7 @@ import android.widget.CompoundButton; import android.widget.DatePicker; import android.widget.EditText; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -104,10 +105,34 @@ public class SelectShareFragment extends SelectRecyclerFragment<Share> { } private void displayShareInfo(final Share share) { - String message = context.getResources().getString(R.string.share_info, - share.getUsername(), (share.getDescription() != null) ? share.getDescription() : "", share.getUrl(), - Util.formatDate(share.getCreated()), Util.formatDate(share.getLastVisited()), Util.formatDate(share.getExpires()), share.getVisitCount()); - Util.info(context, share.getName(), message); + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(share.getName()); + + headers.add(R.string.details_owner); + details.add(share.getUsername()); + + headers.add(R.string.details_description); + details.add(share.getDescription()); + + headers.add(R.string.details_url); + details.add(share.getUrl()); + + headers.add(R.string.details_created); + details.add(Util.formatDate(share.getCreated())); + + headers.add(R.string.details_last_played); + details.add(Util.formatDate(share.getLastVisited())); + + headers.add(R.string.details_expiration); + details.add(Util.formatDate(share.getExpires(), false)); + + headers.add(R.string.details_played_count); + details.add(Long.toString(share.getVisitCount())); + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); } private void updateShareInfo(final Share share) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java index 9853e046..b2ac715a 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java @@ -56,6 +56,7 @@ import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.CacheLocationPreference; import github.daneren2005.dsub.view.ErrorDialog; public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -69,7 +70,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared private ListPreference maxVideoBitrateWifi; private ListPreference maxVideoBitrateMobile; private ListPreference networkTimeout; - private EditTextPreference cacheLocation; + private CacheLocationPreference cacheLocation; private ListPreference preloadCountWifi; private ListPreference preloadCountMobile; private ListPreference keepPlayedCount; @@ -137,6 +138,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared xml = R.xml.settings_playback; } else if("servers".equals(name)) { xml = R.xml.settings_servers; + } else if ("cast".equals(name)) { + xml = R.xml.settings_cast; } if(xml != 0) { @@ -205,7 +208,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI); maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE); networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); - cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); + cacheLocation = (CacheLocationPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); keepPlayedCount = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT); @@ -350,6 +353,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared if(theme != null) { theme.setSummary(theme.getEntry()); + } + if(openToTab != null) { openToTab.setSummary(openToTab.getEntry()); } @@ -633,7 +638,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared } catch(Exception e) { Log.w(TAG, "Failed to create " + musicNoMedia, e); } - } else if (nomediaDir.exists()) { + } else if (!hide && nomediaDir.exists()) { if (!nomediaDir.delete()) { Log.w(TAG, "Failed to delete " + nomediaDir); } @@ -664,8 +669,11 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared SharedPreferences.Editor editor = prefs.edit(); editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); editor.commit(); - cacheLocation.setSummary(defaultPath); - cacheLocation.setText(defaultPath); + + if(cacheLocation != null) { + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } } // Clear download queue. diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java index 93e3a93a..a41b9d6f 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java @@ -15,14 +15,16 @@ package github.daneren2005.dsub.fragments; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import github.daneren2005.dsub.R; -import github.daneren2005.dsub.adapter.ArtistAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; +import github.daneren2005.dsub.adapter.SimilarArtistAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; @@ -35,6 +37,8 @@ import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.UpdateView; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -52,18 +56,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - super.onCreateOptionsMenu(menu, menuInflater); - if(!primaryFragment) { - return; - } - - if(info.getMissingArtists().isEmpty()) { - menu.removeItem(R.id.menu_show_missing); - } - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_play_now: @@ -72,9 +64,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { case R.id.menu_shuffle: playAll(true); return true; - case R.id.menu_show_missing: - showMissingArtists(); - break; } return super.onOptionsItemSelected(item); @@ -82,8 +71,10 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { - onCreateContextMenuSupport(menu, menuInflater, updateView, item); - recreateContextMenu(menu); + if(!Artist.MISSING_ID.equals(item.getId())) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + recreateContextMenu(menu); + } } @Override @@ -93,14 +84,21 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { - SubsonicFragment fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); - - replaceFragment(fragment); + if(Artist.MISSING_ID.equals(artist.getId())) { + String url = "http://www.last.fm/music/" + URLEncoder.encode(artist.getName()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + startActivity(intent); + } else { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment); + } } @Override @@ -109,8 +107,22 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public SectionAdapter getAdapter(List<Artist> objects) { - return new ArtistAdapter(context, objects, this); + public SectionAdapter getAdapter(List<Artist> artists) { + if(info.getMissingArtists().isEmpty()) { + return new SimilarArtistAdapter(context, artists, this); + } else { + List<String> headers = new ArrayList<>(); + headers.add(null); + headers.add(context.getResources().getString(R.string.menu_similar_artists_missing)); + + List<Artist> missingArtists = new ArrayList<>(); + for(String artistName: info.getMissingArtists()) { + Artist artist = new Artist(Artist.MISSING_ID, artistName); + missingArtists.add(artist); + } + + return new SimilarArtistAdapter(context, headers, Arrays.asList(artists, missingArtists), this); + } } @Override @@ -124,16 +136,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { return R.string.menu_similar_artists; } - private void showMissingArtists() { - StringBuilder b = new StringBuilder(); - - for(String name: info.getMissingArtists()) { - b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> "); - } - - Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString()); - } - private void playAll(final boolean shuffle) { new RecursiveLoader(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java index c503ec6c..9cc27128 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -20,6 +20,9 @@ package github.daneren2005.dsub.fragments; import android.annotation.TargetApi; import android.app.Activity; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -37,6 +40,7 @@ import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.GestureDetector; import android.view.Menu; @@ -125,6 +129,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR protected boolean artistOverride = false; protected SwipeRefreshLayout refreshLayout; protected boolean firstRun; + protected MenuItem searchItem; + protected SearchView searchView; public SubsonicFragment() { super(); @@ -177,15 +183,36 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR this.context = context; } + protected void onFinishSetupOptionsMenu(final Menu menu) { + searchItem = menu.findItem(R.id.menu_global_search); + if(searchItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + SearchManager searchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(context.getComponentName()); + if(searchableInfo == null) { + Log.w(TAG, "Failed to get SearchableInfo"); + } else { + searchView.setSearchableInfo(searchableInfo); + } + + String currentQuery = getCurrentQuery(); + if(currentQuery != null) { + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchView.setQuery(getCurrentQuery(), false); + } + }); + } + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_global_shuffle: onShuffleRequested(); return true; - case R.id.menu_global_search: - context.onSearchRequested(); - return true; case R.id.menu_exit: exit(); return true; @@ -221,6 +248,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR addToPlaylist(songs); clearSelected(); return true; + case R.id.menu_star:case R.id.menu_unstar: + toggleSelectedStarred(); + return true; } return false; @@ -269,7 +299,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menu.removeItem(R.id.menu_rate); } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else if(!entry.isVideo()) { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_song_context_offline, menu); @@ -281,7 +310,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menu.removeItem(R.id.bookmark_menu_delete); } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_video_context_offline, menu); @@ -290,6 +318,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menuInflater.inflate(R.menu.select_video_context, menu); } } + + MenuItem starMenu = menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star); + if(starMenu != null) { + starMenu.setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); + } + + if(!isShowArtistEnabled() || (!Util.isTagBrowsing(context) && entry.getParent() == null) || (Util.isTagBrowsing(context) && entry.getArtistId() == null)) { + menu.setGroupVisible(R.id.hide_show_artist, false); + } } else if(selected instanceof Artist) { Artist artist = (Artist) selected; if(Util.isOffline(context)) { @@ -323,6 +360,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) { Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null; + if(selectedItem instanceof DownloadFile) { + entry = ((DownloadFile) selectedItem).getSong(); + } List<Entry> songs = new ArrayList<Entry>(1); songs.add(entry); @@ -656,7 +696,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR final int columns = getRecyclerColumnCount(); GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); - GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); if(spanSizeLookup != null) { gridLayoutManager.setSpanSizeLookup(spanSizeLookup); } @@ -671,15 +711,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR layoutManager.setOrientation(LinearLayoutManager.VERTICAL); return layoutManager; } - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { SectionAdapter adapter = getCurrentAdapter(); if(adapter != null) { - int viewType = getCurrentAdapter().getItemViewType(position); + int viewType = adapter.getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -722,6 +762,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if(downloadService == null) { return; } + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); return; @@ -825,6 +866,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if (downloadService == null) { return; } + + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); } @@ -1572,7 +1615,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR .setPositiveButton(R.string.bookmark_action_resume, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - playNow(songs, song, position); + playNow(songs, song, position, playlistName, playlistId); } }) .setNegativeButton(R.string.bookmark_action_start_over, new DialogInterface.OnClickListener() { @@ -1605,7 +1648,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); - playNow(songs, 0); + playNow(songs, 0, playlistName, playlistId); } }); AlertDialog dialog = builder.create(); @@ -1659,15 +1702,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR new LoadingTask<Void>(context) { @Override protected Void doInBackground() throws Throwable { - DownloadService downloadService = getDownloadService(); - if(downloadService == null) { - return null; - } - - downloadService.clear(); - downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); - downloadService.setSuggestedPlaylistName(playlistName, playlistId); - + playNowInTask(entries, song, position, playlistName, playlistId); return null; } @@ -1677,6 +1712,19 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position) { + playNowInTask(entries, song, position, null, null); + } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position, final String playlistName, final String playlistId) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } protected void deleteBookmark(final MusicDirectory.Entry entry, final SectionAdapter adapter) { Util.confirmDialog(context, R.string.bookmark_delete_title, entry.getTitle(), new DialogInterface.OnClickListener() { @@ -1691,7 +1739,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.deleteBookmark(entry, context, null); - new UpdateHelper.EntryInstanceUpdater(entry) { + new UpdateHelper.EntryInstanceUpdater(entry, DownloadService.METADATA_UPDATED_BOOKMARK) { @Override public void update(Entry found) { found.setBookmark(null); @@ -1898,6 +1946,18 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } + protected void toggleSelectedStarred() { + UpdateHelper.toggleStarred(context, getSelectedEntries()); + } + + protected boolean isShowArtistEnabled() { + return false; + } + + protected String getCurrentQuery() { + return null; + } + public abstract class RecursiveLoader extends LoadingTask<Boolean> { protected MusicService musicService; protected static final int MAX_SONGS = 500; @@ -1909,6 +1969,23 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR musicService = MusicServiceFactory.getMusicService(context); } + protected void getSiblingsRecursively(Entry entry) throws Exception { + MusicDirectory parent = new MusicDirectory(); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + parent.setId(entry.getAlbumId()); + } else { + parent.setId(entry.getParent()); + } + + if(parent.getId() == null) { + songs.add(entry); + } else { + MusicDirectory.Entry dir = new Entry(parent.getId()); + dir.setDirectory(true); + parent.addChild(dir); + getSongsRecursively(parent, songs); + } + } protected void getSongsRecursively(List<Entry> entry) throws Exception { getSongsRecursively(entry, false); } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java index dab104bd..dde76624 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java @@ -20,6 +20,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import github.daneren2005.dsub.R; @@ -78,7 +80,7 @@ public class UserFragment extends SelectRecyclerFragment<User.Setting>{ @Override public SectionAdapter<User.Setting> getAdapter(List<User.Setting> objs) { - return new SettingsAdapter(context, user, getImageLoader(), UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), this); + return SettingsAdapter.getSettingsAdapter(context, user, getImageLoader(), this); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java index c5632362..0f6975ba 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java @@ -166,7 +166,7 @@ public class DLNARouteProvider extends MediaRouteProvider { DLNADevice device = deviceEntry.getValue(); int volume; - if(device.volumeMax == 0) { + if(device.volumeMax <= 0) { volume = 5; } else { int increments = (int) Math.ceil(device.volumeMax / 10.0); diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java index f91c364e..ba8c80c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java @@ -58,6 +58,10 @@ public class DSubSearchProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if(selectionArgs[0].isEmpty()) { + return null; + } + String query = selectionArgs[0] + "*"; SearchResult searchResult = search(query); return createCursor(selectionArgs[0], searchResult); diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java index 18660fa2..5c90c250 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java @@ -278,7 +278,7 @@ public class DSubWidgetProvider extends AppWidgetProvider { private void linkButtons(Context context, RemoteViews views, boolean playerActive) { Intent intent = new Intent(context, SubsonicFragmentActivity.class); intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); diff --git a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java index d579ef54..ece8ee95 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java @@ -33,7 +33,11 @@ import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -48,8 +52,12 @@ public class AutoMediaBrowserService extends MediaBrowserService { private static final String BROWSER_ALBUM_LISTS = "albumLists"; private static final String BROWSER_LIBRARY = "library"; private static final String BROWSER_PLAYLISTS = "playlists"; + private static final String BROWSER_PODCASTS = "podcasts"; + private static final String BROWSER_BOOKMARKS = "bookmarks"; private static final String PLAYLIST_PREFIX = "pl-"; + private static final String PODCAST_PREFIX = "po-"; private static final String ALBUM_TYPE_PREFIX = "ty-"; + private static final String MUSIC_DIRECTORY_PREFIX = "md-"; private DownloadService downloadService; private Handler handler = new Handler(); @@ -76,12 +84,23 @@ public class AutoMediaBrowserService extends MediaBrowserService { } else if(parentId.startsWith(ALBUM_TYPE_PREFIX)) { int id = Integer.valueOf(parentId.substring(ALBUM_TYPE_PREFIX.length())); getAlbumList(result, id); + } else if(parentId.startsWith(MUSIC_DIRECTORY_PREFIX)) { + String id = parentId.substring(MUSIC_DIRECTORY_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_ID); } else if(BROWSER_LIBRARY.equals(parentId)) { getLibrary(result); } else if(BROWSER_PLAYLISTS.equals(parentId)) { getPlaylists(result); } else if(parentId.startsWith(PLAYLIST_PREFIX)) { - getPlayOptions(result, parentId.substring(PLAYLIST_PREFIX.length()), Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + String id = parentId.substring(PLAYLIST_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + } else if(BROWSER_PODCASTS.equals(parentId)) { + getPodcasts(result); + } else if(parentId.startsWith(PODCAST_PREFIX)) { + String id = parentId.substring(PODCAST_PREFIX.length()); + getPodcastEpisodes(result, id); + } else if(BROWSER_BOOKMARKS.equals(parentId)) { + getBookmarks(result); } else { // No idea what it is, send empty result result.sendResult(new ArrayList<MediaBrowser.MediaItem>()); @@ -91,12 +110,12 @@ public class AutoMediaBrowserService extends MediaBrowserService { private void getRootFolders(Result<List<MediaBrowser.MediaItem>> result) { List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); - /*MediaDescription.Builder albumLists = new MediaDescription.Builder(); + MediaDescription.Builder albumLists = new MediaDescription.Builder(); albumLists.setTitle(downloadService.getString(R.string.main_albums_title)) .setMediaId(BROWSER_ALBUM_LISTS); mediaItems.add(new MediaBrowser.MediaItem(albumLists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); - MediaDescription.Builder library = new MediaDescription.Builder(); + /*MediaDescription.Builder library = new MediaDescription.Builder(); library.setTitle(downloadService.getString(R.string.button_bar_browse)) .setMediaId(BROWSER_LIBRARY); mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));*/ @@ -106,6 +125,20 @@ public class AutoMediaBrowserService extends MediaBrowserService { .setMediaId(BROWSER_PLAYLISTS); mediaItems.add(new MediaBrowser.MediaItem(playlists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_podcasts)) + .setMediaId(BROWSER_PODCASTS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_bookmarks)) + .setMediaId(BROWSER_BOOKMARKS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + result.sendResult(mediaItems); } @@ -113,15 +146,10 @@ public class AutoMediaBrowserService extends MediaBrowserService { List<Integer> albums = new ArrayList<>(); albums.add(R.string.main_albums_newest); albums.add(R.string.main_albums_random); - if(ServerInfo.checkServerVersion(downloadService, "1.8")) { - albums.add(R.string.main_albums_alphabetical); - } if(!Util.isTagBrowsing(downloadService)) { albums.add(R.string.main_albums_highest); } - // albums.add(R.string.main_albums_starred); - // albums.add(R.string.main_albums_genres); - // albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_starred); albums.add(R.string.main_albums_recent); albums.add(R.string.main_albums_frequent); @@ -138,8 +166,56 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.sendResult(mediaItems); } - private void getAlbumList(Result<List<MediaBrowser.MediaItem>> result, int id) { + private void getAlbumList(final Result<List<MediaBrowser.MediaItem>> result, final int id) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + String albumListType; + switch(id) { + case R.string.main_albums_newest: + albumListType = "newest"; + break; + case R.string.main_albums_random: + albumListType = "random"; + break; + case R.string.main_albums_highest: + albumListType = "highest"; + break; + case R.string.main_albums_starred: + albumListType = "starred"; + break; + case R.string.main_albums_recent: + albumListType = "recent"; + break; + case R.string.main_albums_frequent: + albumListType = "frequent"; + break; + default: + albumListType = "newest"; + } + + return musicService.getAlbumList(albumListType, 20, 0, true, downloadService, null); + } + + @Override + protected void done(MusicDirectory albumSet) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry album: albumSet.getChildren(true, false)) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(album.getAlbumDisplay()) + .setSubtitle(album.getArtist()) + .setMediaId(MUSIC_DIRECTORY_PREFIX + album.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + result.detach(); } private void getLibrary(Result<List<MediaBrowser.MediaItem>> result) { @@ -172,6 +248,100 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.detach(); } + + private void getPodcasts(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<List<PodcastChannel>>(downloadService) { + @Override + protected List<PodcastChannel> doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastChannels(false, downloadService, null); + } + + @Override + protected void done(List<PodcastChannel> podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(PodcastChannel podcast: podcasts) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getName()) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPodcastEpisodes(final Result<List<MediaBrowser.MediaItem>> result, final String podcastId) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastEpisodes(false, podcastId, downloadService, null); + } + + @Override + protected void done(MusicDirectory podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: podcasts.getChildren(false, true)) { + PodcastEpisode podcast = (PodcastEpisode) entry; + Bundle podcastExtras = new Bundle(); + podcastExtras.putSerializable(Constants.INTENT_EXTRA_ENTRY, podcast); + podcastExtras.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, podcast.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getTitle()) + .setSubtitle(Util.formatDate(downloadService, podcast.getDate(), false)) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .setExtras(podcastExtras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + + private void getBookmarks(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getBookmarks(false, downloadService, null); + } + + @Override + protected void done(MusicDirectory bookmarkList) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: bookmarkList.getChildren(false, true)) { + Bundle extras = new Bundle(); + extras.putSerializable(Constants.INTENT_EXTRA_ENTRY, entry); + extras.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setSubtitle(Util.formatDuration(entry.getBookmark().getPosition() / 1000)) + .setMediaId(entry.getId()) + .setExtras(extras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) { List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); @@ -194,7 +364,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { .setExtras(shuffleExtras); mediaItems.add(new MediaBrowser.MediaItem(shuffle.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); - /*Bundle playLastExtras = new Bundle(); + Bundle playLastExtras = new Bundle(); playLastExtras.putString(idConstant, id); playLastExtras.putBoolean(Constants.INTENT_EXTRA_PLAY_LAST, true); @@ -202,7 +372,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { playLast.setTitle(downloadService.getString(R.string.menu_play_last)) .setMediaId("playLast-" + id) .setExtras(playLastExtras); - mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));*/ + mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); result.sendResult(mediaItems); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java index 53433f5c..1a17dfb3 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java @@ -69,6 +69,9 @@ public class CachedMusicService implements MusicService { private static final int MUSIC_DIR_CACHE_SIZE = 20; private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes + public static final int CACHE_UPDATE_LIST = 1; + public static final int CACHE_UPDATE_METADATA = 2; + private static final int CACHED_LAST_FM = 24 * 60; private final RESTMusicService musicService; private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS); @@ -121,11 +124,13 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class); } - + if(result == null) { result = musicService.getMusicFolders(refresh, context, progressListener); FileUtil.serialize(context, new ArrayList<MusicFolder>(result), getCacheName(context, "musicFolders")); } + + MusicFolder.sort(result); cachedMusicFolders.set(result); } return result; @@ -150,7 +155,7 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, name, Indexes.class); } - + if(result == null) { result = musicService.getIndexes(musicFolderId, refresh, context, progressListener); FileUtil.serialize(context, result, name); @@ -169,12 +174,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getMusicDirectory(id, name, true, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); return null; @@ -185,7 +191,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -201,7 +210,7 @@ public class CachedMusicService implements MusicService { dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); - + // If a cached copy exists to check against, look for removes deleteRemovedEntries(context, dir, cached); } @@ -234,7 +243,7 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); } } } @@ -267,12 +276,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getAlbum(id, name, refresh, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "album", id)); return null; @@ -283,7 +293,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -656,6 +669,11 @@ public class CachedMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongList(type, size, offset, context, progressListener); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { return musicService.getRandomSongs(size, artistId, context, progressListener); } @@ -905,8 +923,18 @@ public class CachedMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - return musicService.getNewestPodcastEpisodes(count, context, progressListener); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + MusicDirectory result = null; + + String cacheName = getCacheName(context, "newestPodcastEpisodes"); + try { + result = musicService.getNewestPodcastEpisodes(refresh, context, progressListener, count); + FileUtil.serialize(context, result, cacheName); + } catch(IOException e) { + result = FileUtil.deserialize(context, cacheName, MusicDirectory.class, 24); + } finally { + return result; + } } @Override @@ -1136,12 +1164,22 @@ public class CachedMusicService implements MusicService { String cacheName = getCacheName(context, "artistInfo", id); ArtistInfo info = null; if(!refresh) { - info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class, CACHED_LAST_FM); } if(info == null && allowNetwork) { - info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); - FileUtil.serialize(context, info, cacheName); + try { + info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); + FileUtil.serialize(context, info, cacheName); + } catch(Exception e) { + Log.w(TAG, "Failed to refresh Artist Info"); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + + // Nothing ever cached, throw error further upstream + if(info == null) { + throw e; + } + } } return info; diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java index 670ea7d2..c2007139 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java @@ -62,6 +62,8 @@ public class ChromeCastController extends RemoteController { private boolean error = false; private boolean ignoreNextPaused = false; private String sessionId; + private boolean isStopping = false; + private Runnable afterUpdateComplete = null; private ServerProxy proxy; private String rootLocation; @@ -247,18 +249,38 @@ public class ChromeCastController extends RemoteController { } } - void startSong(DownloadFile currentPlaying, boolean autoStart, int position) { + void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { if(currentPlaying == null) { try { - if (mediaPlayer != null && !error) { - mediaPlayer.stop(apiClient); + if (mediaPlayer != null && !error && !isStopping) { + isStopping = true; + mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { + isStopping = false; + + if(afterUpdateComplete != null) { + afterUpdateComplete.run(); + afterUpdateComplete = null; + } + } + }); } } catch(Exception e) { // Just means it didn't need to be stopped } downloadService.setPlayerState(PlayerState.IDLE); return; + } else if(isStopping) { + afterUpdateComplete = new Runnable() { + @Override + public void run() { + startSong(currentPlaying, autoStart, position); + } + }; + return; } + downloadService.setPlayerState(PlayerState.PREPARING); MusicDirectory.Entry song = currentPlaying.getSong(); @@ -503,7 +525,7 @@ public class ChromeCastController extends RemoteController { try { Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); - } catch (IOException e) { + } catch (Exception e) { Log.e(TAG, "Exception while creating channel", e); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java index 0673cdeb..24890057 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java @@ -163,7 +163,10 @@ public class DLNAController extends RemoteController { protected void eventReceived(GENASubscription genaSubscription) { Map<String, StateVariableValue> m = genaSubscription.getCurrentValues(); try { - LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString()); + String lastChangeText = m.get("LastChange").toString(); + lastChangeText = lastChangeText.replace(",X_DLNA_SeekTime","").replace(",X_DLNA_SeekByte", ""); + LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeText); + if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) { return; } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java index 3febfaea..e4bab798 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java @@ -84,6 +84,9 @@ public class DownloadFile implements BufferFile { public MusicDirectory.Entry getSong() { return song; } + public boolean isSong() { + return song.isSong(); + } public Context getContext() { return context; @@ -111,7 +114,7 @@ public class DownloadFile implements BufferFile { } } else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) { // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs) - if(song.getBitRate() != null && br > song.getBitRate()) { + if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) { br = song.getBitRate(); } } @@ -379,6 +382,20 @@ public class DownloadFile implements BufferFile { return "DownloadFile (" + song + ")"; } + // Don't do this. Causes infinite loop if two instances of same song + /*@Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DownloadFile downloadFile = (DownloadFile) o; + return Util.equals(this.getSong(), downloadFile.getSong()); + }*/ + private class DownloadTask extends SilentBackgroundTask<Void> { private MusicService musicService; @@ -515,8 +532,9 @@ public class DownloadFile implements BufferFile { } // Only run these if not interrupted, ie: cancelled - if(!isCancelled()) { - new CacheCleaner(context, DownloadService.getInstance()).cleanSpace(); + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !isCancelled()) { + new CacheCleaner(context, downloadService).cleanSpace(); checkDownloads(); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java index 6641d040..3d3a23b8 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java @@ -87,6 +87,7 @@ import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.util.Log; import android.support.v4.util.LruCache; +import android.view.KeyEvent; /** * @author Sindre Mehus @@ -116,6 +117,7 @@ public class DownloadService extends Service { public static final int METADATA_UPDATED_STAR = 1; public static final int METADATA_UPDATED_RATING = 2; public static final int METADATA_UPDATED_BOOKMARK = 4; + public static final int METADATA_UPDATED_COVER_ART = 8; private RemoteControlClientBase mRemoteControl; @@ -219,14 +221,14 @@ public class DownloadService extends Service { } }); - try { + /*try { Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(i); } catch(Throwable e) { // Froyo or lower - } + }*/ effectsController = new AudioEffectsController(DownloadService.this, audioSessionId); if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { @@ -573,7 +575,6 @@ public class DownloadService extends Service { public synchronized void setShufflePlayEnabled(boolean enabled) { shufflePlay = enabled; if (shufflePlay) { - clear(); checkDownloads(); } SharedPreferences.Editor editor = Util.getPreferences(this).edit(); @@ -600,6 +601,9 @@ public class DownloadService extends Service { } editor.commit(); } + public boolean isArtistRadio() { + return artistRadio; + } public synchronized void shuffle() { Collections.shuffle(downloadList); @@ -739,7 +743,7 @@ public class DownloadService extends Service { int position = getPlayerPosition(); int duration = getPlayerDuration(); boolean cutoff = isPastCutoff(position, duration, true); - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { currentPlaying.delete(); } @@ -757,7 +761,7 @@ public class DownloadService extends Service { checkAddBookmark(); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } reset(); @@ -781,6 +785,10 @@ public class DownloadService extends Service { suggestedPlaylistName = null; suggestedPlaylistId = null; + + setShufflePlayEnabled(false); + setArtistRadio(null); + checkDownloads(); } public synchronized void remove(int which) { @@ -807,6 +815,8 @@ public class DownloadService extends Service { if(downloadFile == nextPlaying) { setNextPlaying(); } + + checkDownloads(); } public synchronized void removeBackground(DownloadFile downloadFile) { if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { @@ -853,7 +863,10 @@ public class DownloadService extends Service { if (currentPlaying != null && currentPlaying.getSong() != null) { Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + + if(mRemoteControl != null) { + mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + } } else { Util.broadcastNewTrackInfo(this, null); Notifications.hidePlayingNotification(this, this, handler); @@ -1033,6 +1046,8 @@ public class DownloadService extends Service { bufferAndPlay(position, start); checkDownloads(); setNextPlaying(); + } else { + checkDownloads(); } } } @@ -1120,6 +1135,27 @@ public class DownloadService extends Service { handleError(x); } } + public synchronized int rewind() { + return seekToWrapper(-REWIND); + } + public synchronized int fastForward() { + return seekToWrapper(FAST_FORWARD); + } + protected int seekToWrapper(int difference) { + int msPlayed = Math.max(0, getPlayerPosition()); + Integer duration = getPlayerDuration(); + int msTotal = duration == null ? 0 : duration; + + int seekTo; + if(msPlayed + difference > msTotal) { + seekTo = msTotal; + } else { + seekTo = msPlayed + difference; + } + seekTo(seekTo); + + return seekTo; + } public synchronized void previous() { int index = getCurrentPlayingIndex(); @@ -1128,8 +1164,8 @@ public class DownloadService extends Service { } // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() - REWIND); + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + rewind(); return; } @@ -1154,8 +1190,8 @@ public class DownloadService extends Service { } public synchronized void next(boolean forceCutoff, boolean forceStart) { // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() + FAST_FORWARD); + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + fastForward(); return; } else if(playerState == PREPARING || playerState == PREPARED) { return; @@ -1170,7 +1206,7 @@ public class DownloadService extends Service { } else { cutoff = isPastCutoff(position, duration); } - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { toDelete.add(currentPlaying); } @@ -1179,7 +1215,7 @@ public class DownloadService extends Service { clearCurrentBookmark(true); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } int index = getCurrentPlayingIndex(); @@ -1372,6 +1408,9 @@ public class DownloadService extends Service { if (playerState == PAUSED) { lifecycleSupport.serializeDownloadQueue(); + if(!isPastCutoff()) { + checkAddBookmark(true); + } } boolean show = playerState == PlayerState.STARTED; @@ -1402,9 +1441,9 @@ public class DownloadService extends Service { } if (playerState == STARTED) { - scrobbler.scrobble(this, currentPlaying, false); + scrobbler.scrobble(this, currentPlaying, false, false); } else if (playerState == COMPLETED) { - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); } if(playerState == STARTED && positionCache == null) { @@ -1450,7 +1489,7 @@ public class DownloadService extends Service { positionCache.stop(); positionCache = null; } - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); onStateUpdate(); } @@ -1699,11 +1738,12 @@ public class DownloadService extends Service { nextPlayingTask.cancel(); nextPlayingTask = null; } - } - if(remoteState == LOCAL) { - checkDownloads(); + if(nextPlayerState != IDLE) { + setNextPlayerState(IDLE); + } } + checkDownloads(); if(routeId != null) { final Runnable delayedReconnect = new Runnable() { @@ -2138,7 +2178,7 @@ public class DownloadService extends Service { int preloaded = 0; - if(n != 0 && remoteState == LOCAL) { + if(n != 0 && (remoteState == LOCAL || Util.shouldCacheDuringCasting(this))) { int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); if(start == -1) { start = 0; @@ -2342,7 +2382,7 @@ public class DownloadService extends Service { } // Make cutoff a maximum of 10 minutes - int cutoffPoint = Math.min((int) (duration * DELETE_CUTOFF), 10 * 60 * 1000); + int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000); boolean isPastCutoff = duration > 0 && position > cutoffPoint; // Check to make sure song isn't within 10 seconds of where it was created @@ -2423,8 +2463,11 @@ public class DownloadService extends Service { }.execute(); } } - + private void checkAddBookmark() { + checkAddBookmark(false); + } + private void checkAddBookmark(final boolean updateMetadata) { // Don't do anything if no current playing if(currentPlaying == null || !ServerInfo.canBookmark(this)) { return; @@ -2454,6 +2497,9 @@ public class DownloadService extends Service { if(found != null) { found.setBookmark(new Bookmark(position)); } + if(updateMetadata) { + onMetadataUpdate(METADATA_UPDATED_BOOKMARK); + } return null; } @@ -2482,9 +2528,9 @@ public class DownloadService extends Service { SharedPreferences prefs = Util.getPreferences(this); try { - float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ float adjust = 0f; if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) { + float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ boolean singleAlbum = false; String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1"); @@ -2571,6 +2617,11 @@ public class DownloadService extends Service { onMetadataUpdate(METADATA_UPDATED_STAR); } } + + @Override + public void starCommited(boolean starred) { + + } }); } public void toggleRating(int rating) { @@ -2600,7 +2651,7 @@ public class DownloadService extends Service { UpdateHelper.setRating(this, entry, rating, new UpdateHelper.OnRatingChange() { @Override public void ratingChange(int rating) { - if(currentPlaying == DownloadService.this.currentPlaying) { + if (currentPlaying == DownloadService.this.currentPlaying) { onMetadataUpdate(METADATA_UPDATED_RATING); } } @@ -2613,13 +2664,19 @@ public class DownloadService extends Service { wakeLock.acquire(ms); } + public void handleKeyEvent(KeyEvent keyEvent) { + lifecycleSupport.handleKeyEvent(keyEvent); + } + public void addOnSongChangedListener(OnSongChangedListener listener) { addOnSongChangedListener(listener, false); } - public synchronized void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { - int index = onSongChangedListeners.indexOf(listener); - if(index == -1) { - onSongChangedListeners.add(listener); + public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index == -1) { + onSongChangedListeners.add(listener); + } } if(run) { @@ -2630,6 +2687,7 @@ public class DownloadService extends Service { onSongsChanged(); onSongProgress(); onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); } }); } else { @@ -2637,53 +2695,59 @@ public class DownloadService extends Service { } } } - public synchronized void removeOnSongChangeListener(OnSongChangedListener listener) { - int index = onSongChangedListeners.indexOf(listener); - if(index != -1) { - onSongChangedListeners.remove(index); + public void removeOnSongChangeListener(OnSongChangedListener listener) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index != -1) { + onSongChangedListeners.remove(index); + } } } - private synchronized void onSongChanged() { + private void onSongChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongChanged(currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongChanged(currentPlaying, currentPlayingIndex); - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + } } - } - }); - } + }); + } - if(mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { - mediaPlayerHandler.post(new Runnable() { - @Override - public void run() { - onSongProgress(); - } - }); + if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongProgress(); + } + }); + } } } - private synchronized void onSongsChanged() { + private void onSongsChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + } } - } - }); + }); + } } } - private synchronized void onSongProgress() { + private void onSongProgress() { onSongProgress(true); } private synchronized void onSongProgress(boolean manual) { @@ -2691,22 +2755,27 @@ public class DownloadService extends Service { final Integer duration = getPlayerDuration(); final boolean isSeekable = isSeekable(); final int position = getPlayerPosition(); - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongProgress(currentPlaying, position, duration, isSeekable); + + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongProgress(currentPlaying, position, duration, isSeekable); + } } - } - }); + }); + } } if(manual) { handler.post(new Runnable() { @Override public void run() { - mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + if(mRemoteControl != null) { + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + } } }); } @@ -2718,39 +2787,45 @@ public class DownloadService extends Service { } } } - private synchronized void onStateUpdate() { + private void onStateUpdate() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onStateUpdate(currentPlaying, playerState); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onStateUpdate(currentPlaying, playerState); + } } - } - }); + }); + } } } - private synchronized void onMetadataUpdate() { + public void onMetadataUpdate() { onMetadataUpdate(METADATA_UPDATED_ALL); } - private synchronized void onMetadataUpdate(final int updateType) { - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(instance != null) { - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, updateType); + public void onMetadataUpdate(final int updateType) { + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (instance != null) { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } } - } - }); + }); + } } handler.post(new Runnable() { @Override public void run() { - mRemoteControl.metadataChanged(currentPlaying.getSong()); + if(currentPlaying != null) { + mRemoteControl.metadataChanged(currentPlaying.getSong()); + } } }); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java index 4989db40..f8272356 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -56,6 +56,7 @@ import static github.daneren2005.dsub.domain.PlayerState.PREPARING; public class DownloadServiceLifecycleSupport { private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); public static final String FILENAME_DOWNLOADS_SER = "downloadstate2.ser"; + private static final int DEBOUNCE_TIME = 200; private final DownloadService downloadService; private Looper eventLooper; @@ -225,6 +226,7 @@ public class DownloadServiceLifecycleSupport { } editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { downloadService.start(); @@ -384,14 +386,25 @@ public class DownloadServiceLifecycleSupport { return lastChange; } - private void handleKeyEvent(KeyEvent event) { - if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + public void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.fastForward(); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.rewind(); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { switch (event.getKeyCode()) { case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: downloadService.togglePlayPause(); break; case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: if(lastPressTime < (System.currentTimeMillis() - 500)) { lastPressTime = System.currentTimeMillis(); downloadService.togglePlayPause(); @@ -401,11 +414,23 @@ public class DownloadServiceLifecycleSupport { break; case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - downloadService.previous(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.previous(); + } break; case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_NEXT: - downloadService.next(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + downloadService.rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + downloadService.fastForward(); break; case RemoteControlClient.FLAG_KEY_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_STOP: diff --git a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java index e9d7cbc8..82ef45e1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java @@ -22,6 +22,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.RemoteStatus; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; +import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.service.parser.SubsonicRESTException; import github.daneren2005.dsub.util.Util; @@ -200,11 +201,15 @@ public class JukeboxController extends RemoteController { // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); + int currentPlayingIndex = downloadService.getCurrentPlayingIndex(); if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) { downloadService.setPlayerState(PlayerState.COMPLETED); downloadService.setCurrentPlaying(index, true); if(jukeboxStatus.isPlaying()) { downloadService.setPlayerState(PlayerState.STARTED); + } else if(index == 0 && currentPlayingIndex == downloadService.size() - 1 && downloadService.getRepeatMode() == RepeatMode.ALL) { + // Jukebox does not support any form of auto repeat + start(); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java index 2972bb7c..22f154c4 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java @@ -94,6 +94,8 @@ public interface MusicService { MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception; MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; @@ -147,7 +149,7 @@ public interface MusicService { MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception; - MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception; void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception; diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java index b8633624..e004101d 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -95,7 +95,7 @@ public class OfflineMusicService implements MusicService { artist.setIndex(file.getName().substring(0, 1)); artist.setName(file.getName()); artists.add(artist); - } else { + } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { entries.add(createEntry(context, file)); } } @@ -307,7 +307,15 @@ public class OfflineMusicService implements MusicService { } } }); - + + // Respect counts in search criteria + int artistCount = Math.min(artists.size(), criteria.getArtistCount()); + int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); + int songCount = Math.min(songs.size(), criteria.getSongCount()); + artists = artists.subList(0, artistCount); + albums = albums.subList(0, albumCount); + songs = songs.subList(0, songCount); + return new SearchResult(artists, albums, songs); } @@ -359,20 +367,13 @@ public class OfflineMusicService implements MusicService { } } private int matchCriteria(SearchCritera criteria, String name) { - String query = criteria.getQuery().toLowerCase(); - String[] queryParts = query.split(" "); - String[] nameParts = name.toLowerCase().split(" "); - - int closeness = 0; - for(String queryPart : queryParts) { - for(String namePart : nameParts) { - if(namePart.equals(queryPart)) { - closeness++; - } - } + if (criteria.getPattern().matcher(name).matches()) { + return Util.getStringDistance( + criteria.getQuery().toLowerCase(), + name.toLowerCase()); + } else { + return 0; } - - return closeness; } @Override @@ -587,6 +588,11 @@ public class OfflineMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(ERRORMSG); } @@ -769,7 +775,7 @@ public class OfflineMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { throw new OfflineException(ERRORMSG); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index 4a6e5108..1f9e5494 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -72,7 +72,8 @@ import android.util.Log; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; -import github.daneren2005.dsub.service.parser.AlbumListParser; +import github.daneren2005.dsub.fragments.MainFragment; +import github.daneren2005.dsub.service.parser.EntryListParser; import github.daneren2005.dsub.service.parser.ArtistInfoParser; import github.daneren2005.dsub.service.parser.BookmarkParser; import github.daneren2005.dsub.service.parser.ChatMessageParser; @@ -95,6 +96,7 @@ import github.daneren2005.dsub.service.parser.SearchResult2Parser; import github.daneren2005.dsub.service.parser.SearchResultParser; import github.daneren2005.dsub.service.parser.ShareParser; import github.daneren2005.dsub.service.parser.StarredListParser; +import github.daneren2005.dsub.service.parser.TopSongsParser; import github.daneren2005.dsub.service.parser.UserParser; import github.daneren2005.dsub.service.parser.VideosParser; import github.daneren2005.dsub.service.ssl.SSLSocketFactory; @@ -571,7 +573,7 @@ public class RESTMusicService implements MusicService { Reader reader = getReader(context, progressListener, method, null, names, values, true); try { - return new AlbumListParser(context, getInstance(context)).parse(reader, progressListener); + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -606,7 +608,7 @@ public class RESTMusicService implements MusicService { int decade = Integer.parseInt(extra); // Reverse chronological order only supported in 5.3+ - if(ServerInfo.checkServerVersion(context, "1.13", instance) && ServerInfo.isStockSubsonic(context, instance)) { + if(ServerInfo.checkServerVersion(context, "1.13", instance) && !ServerInfo.isMadsonic(context, instance)) { values.add(decade + 9); values.add(decade); } else { @@ -637,13 +639,49 @@ public class RESTMusicService implements MusicService { Reader reader = getReader(context, progressListener, method, null, names, values, true); try { - return new AlbumListParser(context, instance).parse(reader, progressListener); + return new EntryListParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } - @Override + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + String method; + switch(type) { + case MainFragment.SONGS_NEWEST: + method = "getNewaddedSongs"; + break; + case MainFragment.SONGS_TOP_PLAYED: + method = "getTopplayedSongs"; + break; + case MainFragment.SONGS_RECENT: + method = "getLastplayedSongs"; + break; + case MainFragment.SONGS_FREQUENT: + method = "getMostplayedSongs"; + break; + default: + method = "getNewaddedSongs"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.11", "Artist radio is not supported"); @@ -658,7 +696,13 @@ public class RESTMusicService implements MusicService { int instance = getInstance(context); String method; - if(ServerInfo.isMadsonic(context, instance)) { + if(ServerInfo.isMadsonic6(context, instance)) { + if (Util.isTagBrowsing(context, instance)) { + method = "getSimilarSongsID3"; + } else { + method = "getSimilarSongs"; + } + } else if(ServerInfo.isMadsonic(context, instance)) { method = "getPandoraSongs"; } else { if (Util.isTagBrowsing(context, instance)) { @@ -909,10 +953,9 @@ public class RESTMusicService implements MusicService { StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); builder.append("&id=").append(song.getId()); - // If we are doing mp3 to mp3, just specify raw so that stuff works better - if("mp3".equals(song.getSuffix()) && (song.getTranscodedSuffix() == null || "mp3".equals(song.getTranscodedSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { + // Allow user to specify to stream raw formats if available + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_STREAM_ORIGINAL, true) && ("mp3".equals(song.getSuffix()) || "flac".equals(song.getSuffix()) || "wav".equals(song.getSuffix()) || "aac".equals(song.getSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { builder.append("&format=raw"); - builder.append("&estimateContentLength=true"); } else { builder.append("&maxBitRate=").append(maxBitrate); } @@ -1258,7 +1301,7 @@ public class RESTMusicService implements MusicService { String method = ServerInfo.isMadsonic(context, getInstance(context)) ? "getTopTrackSongs" : "getTopSongs"; Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); try { - return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); + return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -1299,8 +1342,8 @@ public class RESTMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count)); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count), true); try { return new PodcastEntryParser(context, getInstance(context)).parse(null, reader, progressListener); @@ -1462,6 +1505,15 @@ public class RESTMusicService implements MusicService { values.add(setting.getValue()); } + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + Reader reader = getReader(context, progressListener, "createUser", null, names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1487,6 +1539,15 @@ public class RESTMusicService implements MusicService { } } + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + Reader reader = getReader(context, progressListener, "updateUser", null, names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1714,13 +1775,13 @@ public class RESTMusicService implements MusicService { int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); int retry = 0; for(int i = 1; i <= count; i++) { - String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); - long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); - if(id != null) { - scrobble(id, true, time, context, progressListener); - } else { - String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); - try{ + try { + String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); + long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); + if(id != null) { + scrobble(id, true, time, context, progressListener); + } else { + String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); SearchCritera critera = new SearchCritera(search, 0, 0, 1); SearchResult result = searchNew(critera, context, progressListener); if(result.getSongs().size() == 1){ @@ -1732,10 +1793,10 @@ public class RESTMusicService implements MusicService { throw new Exception("Song not found on server"); } } - catch(Exception e){ - Log.e(TAG, e.toString()); - retry++; - } + } + catch(Exception e){ + Log.e(TAG, e.toString()); + retry++; } } @@ -1887,7 +1948,7 @@ public class RESTMusicService implements MusicService { for (int i = 0; i < parameterNames.size(); i++) { builder.append("&").append(parameterNames.get(i)).append("="); String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"); - part = part.replaceAll("\\%27", "'"); + part = part.replaceAll("\\%27", "'"); builder.append(part); } url = builder.toString(); diff --git a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java index 99502f5e..6c70496d 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java @@ -25,6 +25,8 @@ import java.util.Iterator; import java.util.concurrent.LinkedBlockingQueue; import github.daneren2005.dsub.domain.RemoteStatus; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; import github.daneren2005.serverproxy.WebProxy; public abstract class RemoteController { @@ -43,7 +45,11 @@ public abstract class RemoteController { // Really is abstract, just don't want to require RemoteController's support it public void changeNextTrack(DownloadFile song) {} public boolean isNextSupported() { - return this.nextSupported; + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK, true)) { + return this.nextSupported; + } else { + return false; + } } public abstract void setVolume(int volume); public abstract void updateVolume(boolean up); diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java index 1d9fecef..168a7777 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java +++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java @@ -3,6 +3,7 @@ package github.daneren2005.dsub.service; import android.content.Context; import android.util.Log; +import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.SongDBHandler; @@ -21,18 +22,18 @@ public class Scrobbler { private String lastSubmission; private String lastNowPlaying; - public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration) { + public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration, boolean isPastCutoff) { // More than 4 minutes if(playerPosition > FOUR_MINUTES) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } // More than 50% played else if(duration > 0 && playerPosition > (duration / 2)) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } } - public void scrobble(final Context context, final DownloadFile song, final boolean submission) { + public void scrobble(final Context context, final DownloadFile song, final boolean submission, final boolean isPastCutoff) { if(song == null) { return; } @@ -55,7 +56,9 @@ public class Scrobbler { new SilentBackgroundTask<Void>(context) { @Override protected Void doInBackground() { - SongDBHandler.getHandler(context).setSongPlayed(song, submission); + if(isPastCutoff) { + SongDBHandler.getHandler(context).setSongPlayed(song, submission); + } // Scrobbling disabled if (!Util.isScrobblingEnabled(context)) { diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java index 664adcfb..d6e1a002 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java @@ -36,6 +36,12 @@ import github.daneren2005.dsub.util.Util; */ public abstract class AbstractParser { private static final String TAG = AbstractParser.class.getSimpleName(); + private static final String SUBSONIC_RESPONSE = "subsonic-response"; + private static final String MADSONIC_RESPONSE = "madsonic-response"; + private static final String SUBSONIC = "subsonic"; + private static final String MADSONIC = "madsonic"; + private static final String AMPACHE = "ampache"; + protected final Context context; protected final int instance; private XmlPullParser parser; @@ -127,21 +133,32 @@ public abstract class AbstractParser { } protected int nextParseEvent() throws Exception { - return parser.next(); + try { + return parser.next(); + } catch(Exception e) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + + throw e; + } } protected String getElementName() { String name = parser.getName(); - if ("subsonic-response".equals(name) || "madsonic-response".equals(name)) { + if (SUBSONIC_RESPONSE.equals(name) || MADSONIC_RESPONSE.equals(name)) { rootElementFound = true; String version = get("version"); if (version != null) { ServerInfo server = new ServerInfo(); server.setRestVersion(new Version(version)); - if("madsonic".equals(get("type")) || "madsonic-response".equals(name)) { + if(MADSONIC.equals(get("type")) || MADSONIC_RESPONSE.equals(name)) { server.setRestType(ServerInfo.TYPE_MADSONIC); - } else if("subsonic".equals(get("type")) && server.checkServerVersion(context, "1.13")) { + } if(AMPACHE.equals(get("type"))) { + server.setRestType(ServerInfo.TYPE_AMPACHE); + } else if(SUBSONIC.equals(get("type")) && server.checkServerVersion(context, "1.13")) { // Oh am I going to regret this server.setRestType(ServerInfo.TYPE_MADSONIC); server.setRestVersion(new Version("2.0.0")); @@ -154,6 +171,11 @@ public abstract class AbstractParser { protected void validate() throws Exception { if (!rootElementFound) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + throw new Exception(context.getResources().getString(R.string.background_task_parse_error)); } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java index 773c241b..f91aaae1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java @@ -29,9 +29,9 @@ import java.io.Reader; /** * @author Sindre Mehus */ -public class AlbumListParser extends MusicDirectoryEntryParser { +public class EntryListParser extends MusicDirectoryEntryParser { - public AlbumListParser(Context context, int instance) { + public EntryListParser(Context context, int instance) { super(context, instance); } @@ -46,7 +46,12 @@ public class AlbumListParser extends MusicDirectoryEntryParser { String name = getElementName(); if ("album".equals(name)) { MusicDirectory.Entry entry = parseEntry(""); - entry.setDirectory(true); + if(get("isDir") == null) { + entry.setDirectory(true); + } + dir.addChild(entry); + } else if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); dir.addChild(entry); } else if ("error".equals(name)) { handleError(); diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java new file mode 100644 index 00000000..c3719782 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java @@ -0,0 +1,58 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + Copyright 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; + +public class TopSongsParser extends MusicDirectoryEntryParser { + + public TopSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + int trackNumber = 1; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setTrack(trackNumber); + dir.addChild(entry); + + trackNumber++; + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +}
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java index e20556c0..fc2ddd7e 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java @@ -16,6 +16,7 @@ package github.daneren2005.dsub.service.parser; import android.content.Context; +import android.util.Log; import org.xmlpull.v1.XmlPullParser; @@ -23,10 +24,16 @@ import java.io.Reader; import java.util.ArrayList; import java.util.List; +import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; +import github.daneren2005.dsub.domain.User.Setting; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ProgressListener; public class UserParser extends AbstractParser { + private static final String TAG = UserParser.class.getSimpleName(); public UserParser(Context context, int instance) { super(context, instance); @@ -35,14 +42,17 @@ public class UserParser extends AbstractParser { public List<User> parse(Reader reader, ProgressListener progressListener) throws Exception { init(reader); List<User> result = new ArrayList<User>(); + List<MusicFolder> musicFolders = null; + User user = null; int eventType; + String tagName = null; do { eventType = nextParseEvent(); if (eventType == XmlPullParser.START_TAG) { - String name = getElementName(); - if ("user".equals(name)) { - User user = new User(); + tagName = getElementName(); + if ("user".equals(tagName)) { + user = new User(); user.setUsername(get("username")); user.setEmail(get("email")); @@ -53,9 +63,31 @@ public class UserParser extends AbstractParser { parseSetting(user, User.LASTFM); result.add(user); - } else if ("error".equals(name)) { + } else if ("error".equals(tagName)) { handleError(); } + } else if(eventType == XmlPullParser.TEXT) { + if("folder".equals(tagName)) { + String id = getText(); + if(musicFolders == null) { + musicFolders = getMusicFolders(); + } + + if(user != null) { + if(user.getMusicFolderSettings() == null) { + for (MusicFolder musicFolder : musicFolders) { + user.addMusicFolder(musicFolder); + } + } + + for(Setting musicFolder: user.getMusicFolderSettings()) { + if(musicFolder.getName().equals(id)) { + musicFolder.setValue(true); + break; + } + } + } + } } } while (eventType != XmlPullParser.END_DOCUMENT); @@ -63,6 +95,11 @@ public class UserParser extends AbstractParser { return result; } + + private List<MusicFolder> getMusicFolders() throws Exception{ + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getMusicFolders(false, context, null); + } private void parseSetting(User user, String name) { String value = get(name); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java index 8da83be1..bcb7b92f 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java @@ -49,7 +49,7 @@ public class MostRecentSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = SyncUtil.getSyncedMostRecent(context, instance); MusicDirectory albumList = musicService.getAlbumList("newest", 20, 0, tagBrowsing, context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java index a0996628..cb3c3877 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java @@ -56,7 +56,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { String serverName = Util.getServerName(context, instance); List<Playlist> remainder = null; @@ -69,6 +69,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { ArrayList<SyncSet> playlistList = SyncUtil.getSyncedPlaylists(context, instance); List<String> updated = new ArrayList<String>(); + String updatedId = null; boolean removed = false; for(int i = 0; i < playlistList.size(); i++) { SyncSet cachedPlaylist = playlistList.get(i); @@ -94,9 +95,13 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { DownloadFile file = new DownloadFile(context, entry, true); String path = file.getCompleteFile().getPath(); while(!file.isSaved() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(file.isSaved() && !updated.contains(playlist.getName())) { updated.add(playlist.getName()); + if(updatedId == null) { + updatedId = playlist.getId(); + } } } @@ -147,7 +152,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } if(updated.size() > 0) { - Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated), updatedId); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java index 985a7267..7afcad25 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java @@ -23,7 +23,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.util.Log; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; @@ -54,7 +53,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance); try { @@ -68,6 +67,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } List<String> updated = new ArrayList<String>(); + String updatedId = null; for(int i = 0; i < podcastList.size(); i++) { SyncSet set = podcastList.get(i); String id = set.id; @@ -80,6 +80,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) { DownloadFile file = new DownloadFile(context, entry, false); while(!file.isCompleteFileAvailable() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); } // Only add if actualy downloaded correctly @@ -87,6 +88,9 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { existingEpisodes.add(entry.getId()); if(!updated.contains(podcasts.getName())) { updated.add(podcasts.getName()); + if(updatedId == null) { + updatedId = podcasts.getId(); + } } } } @@ -104,7 +108,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { // Make sure there are is at least one change before re-syncing if(updated.size() > 0) { FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance)); - Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated), updatedId); } } catch(Exception e) { Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance)); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java index cf985227..0af8886b 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java @@ -20,7 +20,6 @@ package github.daneren2005.dsub.service.sync; import android.annotation.TargetApi; -import android.app.Notification; import android.content.Context; import android.util.Log; @@ -50,7 +49,7 @@ public class StarredSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = new ArrayList<String>(); MusicDirectory starredList = musicService.getStarredList(context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java index 661f126d..4879d032 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java @@ -65,39 +65,54 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - - // Don't try to sync if no network! - if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { - Log.w(TAG, "Not running sync, not connected to network"); + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + Log.w(TAG, "Not running sync: " + invalidMessage); return; } - + // Make sure battery > x% or is charging IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = context.registerReceiver(null, intentFilter); int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - if(status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { + if (status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); - - if((level / (float)scale) < 0.15) { + + if ((level / (float) scale) < 0.15) { Log.w(TAG, "Not running sync, battery too low"); return; } } + executeSync(context); + } + + private String isNetworkValid() { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + + // Don't try to sync if no network! + if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { + return "Not connected to any network"; + } + // Check if user wants to only sync on wifi SharedPreferences prefs = Util.getPreferences(context); if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) { if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - executeSync(context); + return null; } else { - Log.w(TAG, "Not running sync, not connected to wifi"); + return "Not connected to WIFI"; } } else { - executeSync(context); + return null; + } + } + protected void throwIfNetworkInvalid() throws NetworkNotValidException { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + throw new NetworkNotValidException(invalidMessage); } } @@ -106,32 +121,39 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { Log.i(TAG, "Running sync for " + className); long start = System.currentTimeMillis(); int servers = Util.getServerCount(context); - for(int i = 1; i <= servers; i++) { - try { - if(isValidServer(context, i) && Util.isSyncEnabled(context, i)) { - tagBrowsing = Util.isTagBrowsing(context, i); - musicService.setInstance(i); - onExecuteSync(context, i); - } else { - Log.i(TAG, "Skipped sync for " + i); + try { + for (int i = 1; i <= servers; i++) { + try { + throwIfNetworkInvalid(); + + if (isValidServer(context, i) && Util.isSyncEnabled(context, i)) { + tagBrowsing = Util.isTagBrowsing(context, i); + musicService.setInstance(i); + onExecuteSync(context, i); + } else { + Log.i(TAG, "Skipped sync for " + i); + } + } catch (Exception e) { + Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } - } catch(Exception e) { - Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } + } catch (NetworkNotValidException e) { + Log.e(TAG, "Stopped sync due to network loss", e); } Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms"); } - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { } - protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception { + protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException { boolean downloaded = false; for (MusicDirectory.Entry song: parent.getChildren(false, true)) { if (!song.isVideo()) { DownloadFile file = new DownloadFile(context, song, save); while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(!file.isFailed()) { downloaded = true; @@ -171,4 +193,10 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { String url = Util.getRestUrl(context, "null", instance, false); return !(url.contains("demo.subsonic.org") || url.contains("yourhost")); } + + public class NetworkNotValidException extends Throwable { + public NetworkNotValidException(String reason) { + super("Not running sync: " + reason); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java index 2af468f6..bdd961b4 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java @@ -66,7 +66,6 @@ public class ArtistRadioBuffer { buffer.clear(); } - context.clear(); this.artistId = artistId; awaitingResults = true; refill(); @@ -108,7 +107,7 @@ public class ArtistRadioBuffer { } private void refill() { - if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { + if (buffer != null && executorService != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { executorService.shutdown(); return; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java index 18f245d5..31e83200 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java +++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java @@ -176,7 +176,7 @@ public abstract class BackgroundTask<T> implements ProgressListener { } @Override - public void updateCache() { + public void updateCache(int changeCode) { } @@ -208,8 +208,18 @@ public abstract class BackgroundTask<T> implements ProgressListener { handler.post(new Runnable() { @Override public void run() { - if(!isCancelled()) { - onDone(result); + if (!isCancelled()) { + try { + onDone(result); + } catch (Throwable t) { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + } } taskStart.set(false); diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java index 89e7de3b..ec3489ab 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java @@ -67,6 +67,7 @@ public final class Constants { public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks"; public static final String INTENT_EXTRA_SHOW_ALL = "showAll"; public static final String INTENT_EXTRA_PLAY_LAST = "playLast"; + public static final String INTENT_EXTRA_ENTRY = "passedEntry"; // Preferences keys. public static final String PREFERENCES_KEY_SERVER_KEY = "server"; @@ -168,6 +169,11 @@ public final class Constants { public static final String PREFERENCES_KEY_COLOR_ACTION_BAR = "colorActionBar"; public static final String PREFERENCES_KEY_SHUFFLE_BY_ALBUM = "shuffleByAlbum"; public static final String PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER = "neverResumePlayQueue"; + public static final String PREFERENCES_KEY_BATCH_MODE = "batchMode"; + public static final String PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK = "castingGaplessPlayback"; + public static final String PREFERENCES_KEY_CAST_STREAM_ORIGINAL = "castStreamOriginal"; + public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification"; + public static final String PREFERENCES_KEY_CAST_CACHE = "castCache"; public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount"; public static final String OFFLINE_SCROBBLE_ID = "scrobbleID"; diff --git a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java new file mode 100644 index 00000000..d8046d1b --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java @@ -0,0 +1,20 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + Copyright 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.util; + +public final class EnvironmentVariables { + public static final String PASTEBIN_DEV_KEY = ""; +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java index 85844360..a85141df 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java @@ -524,44 +524,49 @@ public class ImageLoader { @Override protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(mContext); - ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); - String url = artistInfo.getImageUrl(); - - // Figure out whether we are going to get a artist image or the standard image - if(url != null && !"".equals(url.trim())) { - // If getting the artist image fails for any reason, retry for the standard version - subTask = new ViewUrlTask(mContext, mView, url, mSize) { - @Override - protected void failedToDownload() { - // Call loadImage so we can take advantage of all of it's logic checks - loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); - - // Delete subTask so it doesn't get called in done - subTask = null; + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); + String url = artistInfo.getImageUrl(); + + // Figure out whether we are going to get a artist image or the standard image + if (url != null && !"".equals(url.trim())) { + // If getting the artist image fails for any reason, retry for the standard version + subTask = new ViewUrlTask(mContext, mView, url, mSize) { + @Override + protected void failedToDownload() { + // Call loadImage so we can take advantage of all of it's logic checks + loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); + + // Delete subTask so it doesn't get called in done + subTask = null; + } + }; + } else { + if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { + // Try to lookup child cover art + MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); + if (firstChild != null) { + mEntry.setCoverArt(firstChild.getCoverArt()); + } } - }; - } else { - if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { - // Try to lookup child cover art - MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); - if (firstChild != null) { - mEntry.setCoverArt(firstChild.getCoverArt()); + + if (mEntry != null && mEntry.getCoverArt() != null) { + subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); + } else { + // If entry is null as well, we need to just set as a blank image + Bitmap bitmap = getUnknownImage(mEntry, mSize); + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + return null; } } - if (mEntry != null && mEntry.getCoverArt() != null) { - subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); - } else { - // If entry is null as well, we need to just set as a blank image - Bitmap bitmap = getUnknownImage(mEntry, mSize); - mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); - return null; - } + // Execute whichever way we decided to go + subTask.doInBackground(); + } catch (Throwable x) { + Log.e(TAG, "Failed to get artist info", x); + cancelled.set(true); } - - // Execute whichever way we decided to go - subTask.doInBackground(); return null; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java index 375c9966..2948844b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java @@ -41,6 +41,7 @@ import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.provider.DSubWidgetProvider; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.view.UpdateView; public final class Notifications { private static final String TAG = Notifications.class.getSimpleName(); @@ -74,6 +75,10 @@ public final class Notifications { } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notification.visibility = Notification.VISIBILITY_PUBLIC; + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HEADS_UP_NOTIFICATION, false) && !UpdateView.hasActiveActivity()) { + notification.vibrate = new long[0]; + } } RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); @@ -82,7 +87,7 @@ public final class Notifications { Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); playShowing = true; @@ -100,7 +105,7 @@ public final class Notifications { handler.post(new Runnable() { @Override public void run() { - if(playing) { + if (playing) { downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); } else { playShowing = false; @@ -117,7 +122,8 @@ public final class Notifications { DSubWidgetProvider.notifyInstances(context, downloadService, playing); } - private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){ + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote) { + boolean isLongFile = song.isAudioBook() || song.isPodcast(); // Use the same text for the ticker and the expanded notification String title = song.getTitle(); @@ -152,20 +158,47 @@ public final class Notifications { if(persistent) { if(expanded) { rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_start); + + if(isLongFile && playing) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } } else { rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_start); - rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + if(isLongFile && playing) { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + } rv.setImageViewResource(R.id.control_next, R.drawable.notification_close); } + } else if(isLongFile) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + // Necessary for switching back since it appears to re-use the same layout + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); } // Create actions for media buttons PendingIntent pendingIntent; - int previous = 0, pause = 0, next = 0, close = 0; + int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 0; if(persistent && !expanded) { pause = R.id.control_previous; - next = R.id.control_pause; + if(isLongFile && playing) { + fastForward = R.id.control_pause; + } else { + next = R.id.control_pause; + } close = R.id.control_next; + } else if(isLongFile && (!persistent || (expanded && playing))) { + rewind = R.id.control_previous; + pause = R.id.control_pause; + fastForward = R.id.control_next; } else { previous = R.id.control_previous; pause = R.id.control_pause; @@ -180,21 +213,28 @@ public final class Notifications { if(previous > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(previous, pendingIntent); } + if(rewind > 0) { + Intent rewindIntent = new Intent("KEYCODE_MEDIA_REWIND"); + rewindIntent.setComponent(new ComponentName(context, DownloadService.class)); + rewindIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)); + pendingIntent = PendingIntent.getService(context, 0, rewindIntent, 0); + rv.setOnClickPendingIntent(rewind, pendingIntent); + } if(pause > 0) { if(playing) { Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE"); pauseIntent.setComponent(new ComponentName(context, DownloadService.class)); - pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } else { Intent prevIntent = new Intent("KEYCODE_MEDIA_START"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } @@ -202,14 +242,21 @@ public final class Notifications { if(next > 0) { Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT"); nextIntent.setComponent(new ComponentName(context, DownloadService.class)); - nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); + nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); rv.setOnClickPendingIntent(next, pendingIntent); } + if(fastForward > 0) { + Intent fastForwardIntent = new Intent("KEYCODE_MEDIA_FAST_FORWARD"); + fastForwardIntent.setComponent(new ComponentName(context, DownloadService.class)); + fastForwardIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + pendingIntent = PendingIntent.getService(context, 0, fastForwardIntent, 0); + rv.setOnClickPendingIntent(fastForward, pendingIntent); + } if(close > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(close, pendingIntent); } @@ -270,7 +317,7 @@ public final class Notifications { Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); builder.setContentIntent(PendingIntent.getActivity(context, 2, notificationIntent, 0)); final Notification notification = builder.build(); @@ -306,6 +353,9 @@ public final class Notifications { } public static void showSyncNotification(final Context context, int stringId, String extra) { + showSyncNotification(context, stringId, extra, null); + } + public static void showSyncNotification(final Context context, int stringId, String extra, String extraId) { if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) { if(extra == null) { extra = ""; @@ -323,7 +373,7 @@ public final class Notifications { .setAutoCancel(true); Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); String tab = null, type = null; switch(stringId) { @@ -346,6 +396,9 @@ public final class Notifications { if(type != null) { notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type); } + if(extraId != null) { + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ID, extraId); + } builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0)); diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java index 22f35efc..603b1ccb 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java @@ -24,5 +24,5 @@ package github.daneren2005.dsub.util; public interface ProgressListener { void updateProgress(String message); void updateProgress(int messageId); - void updateCache(); + void updateCache(int changeCode); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java index 8d91a251..90c885f6 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java +++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java @@ -120,6 +120,7 @@ public class SongDBHandler extends SQLiteOpenHelper { values.put(SONGS_SERVER_KEY, serverKey); values.put(SONGS_SERVER_ID, entry.getFirst()); values.put(SONGS_COMPLETE_PATH, entry.getSecond()); + // Util.sleepQuietly(10000); db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); } @@ -150,11 +151,23 @@ public class SongDBHandler extends SQLiteOpenHelper { db.close(); } + public boolean hasBeenPlayed(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[0] != null && lastPlayed[0] > 0; + } + public boolean hasBeenCompleted(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[1] != null && lastPlayed[1] > 0; + } public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { return getLastPlayed(getOnlineSongId(entry)); } protected synchronized Long[] getLastPlayed(Pair<Integer, String> pair) { - return getLastPlayed(pair.getFirst(), pair.getSecond()); + if(pair == null) { + return null; + } else { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } } public synchronized Long[] getLastPlayed(int serverKey, String id) { SQLiteDatabase db = this.getReadableDatabase(); @@ -169,9 +182,12 @@ public class SongDBHandler extends SQLiteOpenHelper { dates[0] = cursor.getLong(0); dates[1] = cursor.getLong(1); return dates; - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getOnlineSongId(MusicDirectory.Entry entry) { @@ -210,9 +226,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getIdFromPath(int serverKey, String path) { SQLiteDatabase db = this.getReadableDatabase(); @@ -223,9 +242,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public static SongDBHandler getHandler(Context context) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java index c7e0a04b..4cf25b30 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.View; import android.widget.RatingBar; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,9 +52,24 @@ public final class UpdateHelper { } public static void toggleStarred(final Context context, final Entry entry, final OnStarChange onStarChange) { - final boolean starred = !entry.isStarred(); - entry.setStarred(starred); + toggleStarred(context, Arrays.asList(entry), onStarChange); + } + + public static void toggleStarred(Context context, List<Entry> entries) { + toggleStarred(context, entries, null); + } + public static void toggleStarred(final Context context, final List<Entry> entries, final OnStarChange onStarChange) { + if(entries.isEmpty()) { + return; + } + + final Entry firstEntry = entries.get(0); + final boolean starred = !firstEntry.isStarred(); + for(Entry entry: entries) { + entry.setStarred(starred); + } if(onStarChange != null) { + onStarChange.entries = entries; onStarChange.starChange(starred); } @@ -61,22 +77,30 @@ public final class UpdateHelper { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); - if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) { - if(entry.isAlbum()) { - musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context); + List<Entry> songs = new ArrayList<Entry>(); + List<Entry> artists = new ArrayList<Entry>(); + List<Entry> albums = new ArrayList<Entry>(); + for(Entry entry: entries) { + if(entry.isDirectory() && Util.isTagBrowsing(context)) { + if(entry.isAlbum()) { + albums.add(entry); + } else { + artists.add(entry); + } } else { - musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context); + songs.add(entry); } - } else { - musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context); } - - new EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(starred); - } - }.execute(); + musicService.setStarred(songs, artists, albums, starred, this, context); + + for(Entry entry: entries) { + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setStarred(starred); + } + }.execute(); + } return null; } @@ -84,13 +108,21 @@ public final class UpdateHelper { @Override protected void done(Void result) { // UpdateView - Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle())); + int starMsgId = starred ? R.string.starring_content_starred : R.string.starring_content_unstarred; + String starMsgBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + Util.toast(context, context.getResources().getString(starMsgId, starMsgBody)); + + if(onStarChange != null) { + onStarChange.starCommited(starred); + } } @Override protected void error(Throwable error) { Log.w(TAG, "Failed to star", error); - entry.setStarred(!starred); + for(Entry entry: entries) { + entry.setStarred(!starred); + } if(onStarChange != null) { onStarChange.starChange(!starred); } @@ -99,7 +131,8 @@ public final class UpdateHelper { if (error instanceof OfflineException || error instanceof ServerTooOldException) { msg = getErrorMessage(error); } else { - msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error); + String errorBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + msg = context.getResources().getString(R.string.starring_content_error, errorBody) + " " + getErrorMessage(error); } Util.toast(context, msg, false); @@ -215,6 +248,7 @@ public final class UpdateHelper { msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error); } + Log.e(TAG, "Failed to setRating", error); Util.toast(context, msg, false); } }.execute(); @@ -222,10 +256,15 @@ public final class UpdateHelper { public static abstract class EntryInstanceUpdater { private Entry entry; + protected int metadataUpdate = DownloadService.METADATA_UPDATED_ALL; public EntryInstanceUpdater(Entry entry) { this.entry = entry; } + public EntryInstanceUpdater(Entry entry, int metadataUpdate) { + this.entry = entry; + this.metadataUpdate = metadataUpdate; + } public abstract void update(Entry found); @@ -234,11 +273,17 @@ public final class UpdateHelper { if(downloadService != null && !entry.isDirectory()) { boolean serializeChanges = false; List<DownloadFile> downloadFiles = downloadService.getDownloads(); + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + for(DownloadFile file: downloadFiles) { Entry check = file.getSong(); if(entry.getId().equals(check.getId())) { - update(entry); + update(check); serializeChanges = true; + + if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) { + downloadService.onMetadataUpdate(metadataUpdate); + } } } @@ -255,7 +300,10 @@ public final class UpdateHelper { } public static abstract class OnStarChange { + protected List<Entry> entries; + public abstract void starChange(boolean starred); + public abstract void starCommited(boolean starred); } public static abstract class OnRatingChange { public abstract void ratingChange(int rating); diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java index 6417dc81..24d3906b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java @@ -29,8 +29,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.ListView; import android.widget.TextView; import github.daneren2005.dsub.R; @@ -159,8 +157,11 @@ public final class UserUtil { return defaultValue; } - - public static void confirmCredentials(final Activity context, final Runnable onSuccess) { + + public static void confirmCredentials(Activity context, Runnable onSuccess) { + confirmCredentials(context, onSuccess, null); + } + public static void confirmCredentials(final Activity context, final Runnable onSuccess, final Runnable onCancel) { final long currentTime = System.currentTimeMillis(); // If already ran this check within last x time, just go ahead and auth if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) { @@ -175,12 +176,7 @@ public final class UserUtil { .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - String password = passwordView.getText().toString(); - - SharedPreferences prefs = Util.getPreferences(context); - String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); - - if(password != null && password.equals(correctPassword)) { + if(isPasswordCorrect(context, passwordView)) { lastVerifiedTime = currentTime; onSuccess.run(); } else { @@ -188,7 +184,14 @@ public final class UserUtil { } } }) - .setNegativeButton(R.string.common_cancel, null) + .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if(onCancel != null) { + onCancel.run(); + } + } + }) .setCancelable(true); AlertDialog dialog = builder.create(); @@ -199,8 +202,14 @@ public final class UserUtil { public static void changePassword(final Activity context, final User user) { View layout = context.getLayoutInflater().inflate(R.layout.change_password, null); + View currentPasswordLayout = layout.findViewById(R.id.current_password_layout); + final TextView currentPasswordView = (TextView) layout.findViewById(R.id.current_password); final TextView passwordView = (TextView) layout.findViewById(R.id.new_password); + if(isCurrentAdmin()) { + currentPasswordLayout.setVisibility(View.GONE); + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.admin_change_password) .setView(layout) @@ -215,8 +224,12 @@ public final class UserUtil { @Override public void onClick(View v) { final String password = passwordView.getText().toString(); + if(!isCurrentAdmin() && !isPasswordCorrect(context, currentPasswordView)) { + Util.toast(context, R.string.admin_confirm_password_bad); + return; + } // Don't allow blank passwords - if ("".equals(password)) { + else if ("".equals(password)) { Util.toast(context, R.string.admin_change_password_invalid); return; } @@ -252,6 +265,16 @@ public final class UserUtil { }); } + private static boolean isPasswordCorrect(Context context, TextView passwordView) { + return isPasswordCorrect(context, passwordView.getText().toString()); + } + private static boolean isPasswordCorrect(Context context, String password) { + SharedPreferences prefs = Util.getPreferences(context); + String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); + + return password != null && password.equals(correctPassword); + } + public static void updateSettings(final Context context, final User user) { new SilentBackgroundTask<Void>(context) { @Override @@ -373,7 +396,7 @@ public final class UserUtil { }); } - public static void addNewUser(final Activity context, final SubsonicFragment fragment) { + public static void addNewUser(final Activity context, final SubsonicFragment fragment, User sampleUser) { final User user = new User(); for(String role: User.ROLES) { if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) { @@ -383,6 +406,13 @@ public final class UserUtil { } } + if(sampleUser.getMusicFolderSettings() != null) { + for(User.Setting setting: sampleUser.getMusicFolderSettings()) { + User.MusicFolderSetting musicFolderSetting = (User.MusicFolderSetting) setting; + user.addMusicFolder(musicFolderSetting, true); + } + } + View layout = context.getLayoutInflater().inflate(R.layout.create_user, null); final TextView usernameView = (TextView) layout.findViewById(R.id.username); final TextView emailView = (TextView) layout.findViewById(R.id.email); @@ -391,7 +421,7 @@ public final class UserUtil { LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(new SettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { + recyclerView.setAdapter(SettingsAdapter.getSettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { @Override public void onItemClicked(UpdateView<User.Setting> updateView, User.Setting item) { if(updateView.isCheckable()) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java index bf4af20d..9f742ef0 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -22,6 +22,8 @@ import android.app.Activity; import android.graphics.Color; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; +import android.content.ClipboardManager; +import android.content.ClipData; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -46,9 +48,11 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.util.SparseArray; +import android.view.View; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -107,6 +111,7 @@ public final class Util { private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a"); private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a"); + private static SimpleDateFormat DATE_FORMAT_NO_TIME = new SimpleDateFormat("MMM d, yyyy"); private static int CURRENT_YEAR = new Date().getYear(); public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED"; @@ -376,6 +381,12 @@ public final class Util { int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; } + public static boolean isBatchMode(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false); + } + public static void setBatchMode(Context context, boolean batchMode) { + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, batchMode).commit(); + } public static String getRestUrl(Context context, String method) { return getRestUrl(context, method, true); @@ -401,13 +412,15 @@ public final class Util { String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); if(allowAltAddress && Util.isWifiConnected(context)) { String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, ""); - String currentSSID = Util.getSSID(context); - - String[] ssidParts = SSID.split(","); - if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { - String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); - if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { - serverUrl = internalUrl; + if(!SSID.isEmpty()) { + String currentSSID = Util.getSSID(context); + + String[] ssidParts = SSID.split(","); + if ("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { + String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); + if (internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { + serverUrl = internalUrl; + } } } } @@ -683,6 +696,10 @@ public final class Util { editor.commit(); } + public static boolean shouldCacheDuringCasting(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_CACHE, false); + } + public static boolean shouldStartOnHeadphones(Context context) { SharedPreferences prefs = getPreferences(context); return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false); @@ -901,26 +918,42 @@ public final class Util { } public static String formatDate(Context context, String dateString) { + return formatDate(context, dateString, true); + } + public static String formatDate(Context context, String dateString, boolean includeTime) { + if(dateString == null) { + return ""; + } + try { + dateString = dateString.replace(' ', 'T'); boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); if (isDateNormalized) { dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } - return formatDate(dateFormat.parse(dateString)); + return formatDate(dateFormat.parse(dateString), includeTime); } catch(ParseException e) { + Log.e(TAG, "Failed to parse date string", e); return dateString; } } public static String formatDate(Date date) { + return formatDate(date, true); + } + public static String formatDate(Date date, boolean includeTime) { if(date == null) { return "Never"; } else { - if(date.getYear() != CURRENT_YEAR) { - return DATE_FORMAT_LONG.format(date); + if(includeTime) { + if (date.getYear() != CURRENT_YEAR) { + return DATE_FORMAT_LONG.format(date); + } else { + return DATE_FORMAT_SHORT.format(date); + } } else { - return DATE_FORMAT_SHORT.format(date); + return DATE_FORMAT_NO_TIME.format(date); } } } @@ -1222,12 +1255,36 @@ public final class Util { } showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); } - public static void showDetailsDialog(Context context, String title, List<String> headers, List<String> details) { + public static void showDetailsDialog(Context context, String title, List<String> headers, final List<String> details) { ListView listView = new ListView(context); listView.setAdapter(new DetailsAdapter(context, R.layout.details_item, headers, details)); listView.setDivider(null); listView.setScrollbarFadingEnabled(false); + // Let the user long-click on a row to copy its value to the clipboard + final Context contextRef = context; + listView.setOnItemLongClickListener(new ListView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int pos, long id) { + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + if(nameView == null || detailsView == null) { + return false; + } + + CharSequence name = nameView.getText(); + CharSequence value = detailsView.getText(); + + ClipboardManager clipboard = (ClipboardManager) contextRef.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(name, value); + clipboard.setPrimaryClip(clip); + + toast(contextRef, "Copied " + name + " to clipboard"); + + return true; + } + }); + new AlertDialog.Builder(context) // .setIcon(android.R.drawable.ic_dialog_info) .setTitle(title) @@ -1367,72 +1424,79 @@ public final class Util { * <p>Broadcasts the given song info as the new song being played.</p> */ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { - DownloadService downloadService = (DownloadService)context; - Intent intent = new Intent(EVENT_META_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); - - if (song != null) { - intent.putExtra("title", song.getTitle()); - intent.putExtra("artist", song.getArtist()); - intent.putExtra("album", song.getAlbum()); - - File albumArtFile = FileUtil.getAlbumArtFile(context, song); - intent.putExtra("coverart", albumArtFile.getAbsolutePath()); - avrcpIntent.putExtra("playing", true); - } else { - intent.putExtra("title", ""); - intent.putExtra("artist", ""); - intent.putExtra("album", ""); - intent.putExtra("coverart", ""); - avrcpIntent.putExtra("playing", false); - } - addTrackInfo(context, song, avrcpIntent); + try { + Intent intent = new Intent(EVENT_META_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); + + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); - context.sendBroadcast(intent); - context.sendBroadcast(avrcpIntent); + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + avrcpIntent.putExtra("playing", true); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + avrcpIntent.putExtra("playing", false); + } + addTrackInfo(context, song, avrcpIntent); + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastNewTrackInfo", e); + } } /** * <p>Broadcasts the given player state as the one being set.</p> */ public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) { - Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); - - switch (state) { - case STARTED: - intent.putExtra("state", "play"); - avrcpIntent.putExtra("playing", true); - break; - case STOPPED: - intent.putExtra("state", "stop"); - avrcpIntent.putExtra("playing", false); - break; - case PAUSED: - intent.putExtra("state", "pause"); - avrcpIntent.putExtra("playing", false); - break; - case PREPARED: - // Only send quick pause event for samsung devices, causes issues for others - if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + try { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + intent.putExtra("state", "stop"); avrcpIntent.putExtra("playing", false); - } else { - return; // Don't broadcast anything - } - break; - case COMPLETED: - intent.putExtra("state", "complete"); - avrcpIntent.putExtra("playing", false); - break; - default: - return; // No need to broadcast. - } - addTrackInfo(context, song, avrcpIntent); + break; + case PAUSED: + intent.putExtra("state", "pause"); + avrcpIntent.putExtra("playing", false); + break; + case PREPARED: + // Only send quick pause event for samsung devices, causes issues for others + if (Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + avrcpIntent.putExtra("playing", false); + } else { + return; // Don't broadcast anything + } + break; + case COMPLETED: + intent.putExtra("state", "complete"); + avrcpIntent.putExtra("playing", false); + break; + default: + return; // No need to broadcast. + } + addTrackInfo(context, song, avrcpIntent); - if(state != PlayerState.PREPARED) { - context.sendBroadcast(intent); + if (state != PlayerState.PREPARED) { + context.sendBroadcast(intent); + } + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e); } - context.sendBroadcast(avrcpIntent); } private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java index 456446f3..df468155 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java @@ -34,16 +34,23 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.support.annotation.NonNull; import android.support.v7.media.MediaRouter; +import android.util.Log; +import android.view.KeyEvent; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.activity.SubsonicActivity; import github.daneren2005.dsub.activity.SubsonicFragmentActivity; +import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.SearchCritera; import github.daneren2005.dsub.domain.SearchResult; import github.daneren2005.dsub.service.DownloadFile; @@ -87,7 +94,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { Intent activityIntent = new Intent(context, SubsonicFragmentActivity.class); activityIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0); mediaSession.setSessionActivity(activityPendingIntent); @@ -141,11 +148,17 @@ public class RemoteControlClientLP extends RemoteControlClientBase { position = downloadService.getPlayerPosition(); } builder.setState(newState, position, 1.0f); - builder.setActions(getPlaybackActions()); - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + Entry entry = null; + boolean isSong = true; if(downloadFile != null) { - MusicDirectory.Entry entry = downloadFile.getSong(); + entry = downloadFile.getSong(); + isSong = entry.isSong(); + } + + builder.setActions(getPlaybackActions(isSong)); + + if(entry != null) { addCustomActions(entry, builder); builder.setActiveQueueItemId(entry.getId().hashCode()); } @@ -156,7 +169,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateMetadata(Context context, MusicDirectory.Entry currentSong) { + public void updateMetadata(Context context, Entry currentSong) { setMetadata(currentSong, null); if(currentSong != null && imageLoader != null) { @@ -165,11 +178,11 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void metadataChanged(MusicDirectory.Entry currentSong) { + public void metadataChanged(Entry currentSong) { setPlaybackState(previousState); } - public void setMetadata(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void setMetadata(Entry currentSong, Bitmap bitmap) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) .putString(MediaMetadata.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) @@ -189,7 +202,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void updateAlbumArt(Entry currentSong, Bitmap bitmap) { setMetadata(currentSong, bitmap); } @@ -208,7 +221,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { List<MediaSession.QueueItem> queue = new ArrayList<>(); for(DownloadFile file: playlist) { - MusicDirectory.Entry entry = file.getSong(); + Entry entry = file.getSong(); MediaDescription description = new MediaDescription.Builder() .setMediaId(entry.getId()) @@ -227,7 +240,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return mediaSession; } - protected long getPlaybackActions() { + protected long getPlaybackActions(boolean isSong) { long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO | @@ -235,16 +248,21 @@ public class RemoteControlClientLP extends RemoteControlClientBase { int currentIndex = downloadService.getCurrentPlayingIndex(); int size = downloadService.size(); - if(currentIndex > 0) { + if(isSong) { + if (currentIndex > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if (currentIndex < size - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + } else { actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; - } - if(currentIndex < size - 1) { actions |= PlaybackState.ACTION_SKIP_TO_NEXT; } return actions; } - protected void addCustomActions(MusicDirectory.Entry currentSong, PlaybackState.Builder builder) { + protected void addCustomActions(Entry currentSong, PlaybackState.Builder builder) { Bundle showOnWearExtras = new Bundle(); showOnWearExtras.putBoolean(SHOW_ON_WEAR, true); @@ -296,7 +314,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { SearchResult results = musicService.search(searchCritera, downloadService, null); if(results.hasArtists()) { - playFromParent(new MusicDirectory.Entry(results.getArtists().get(0))); + playFromParent(new Entry(results.getArtists().get(0))); } else if(results.hasAlbums()) { playFromParent(results.getAlbums().get(0)); } else if(results.hasSongs()) { @@ -307,13 +325,13 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return null; } - - private void playFromParent(MusicDirectory.Entry parent) throws Exception { - List<MusicDirectory.Entry> songs = new ArrayList<>(); + + private void playFromParent(Entry parent) throws Exception { + List<Entry> songs = new ArrayList<>(); getSongsRecursively(parent, songs); playSongs(songs); } - private void getSongsRecursively(MusicDirectory.Entry parent, List<MusicDirectory.Entry> songs) throws Exception { + private void getSongsRecursively(Entry parent, List<Entry> songs) throws Exception { MusicDirectory musicDirectory; if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { musicDirectory = musicService.getAlbum(parent.getId(), parent.getTitle(), false, downloadService, this); @@ -321,7 +339,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { musicDirectory = musicService.getMusicDirectory(parent.getId(), parent.getTitle(), false, downloadService, this); } - for (MusicDirectory.Entry dir : musicDirectory.getChildren(true, false)) { + for (Entry dir : musicDirectory.getChildren(true, false)) { if (dir.getRating() == 1) { continue; } @@ -329,7 +347,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { getSongsRecursively(dir, songs); } - for (MusicDirectory.Entry song : musicDirectory.getChildren(false, true)) { + for (Entry song : musicDirectory.getChildren(false, true)) { if (!song.isVideo() && song.getRating() != 1) { songs.add(song); } @@ -349,20 +367,69 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } }.execute(); } + private void playMusicDirectory(Entry dir, boolean shuffle, boolean append, boolean playFromBookmark) { + playMusicDirectory(dir.getId(), shuffle, append, playFromBookmark); + } + private void playMusicDirectory(final String dirId, final boolean shuffle, final boolean append, final boolean playFromBookmark) { + new SilentServiceTask<Void>(downloadService) { + @Override + protected Void doInBackground(MusicService musicService) throws Throwable { + MusicDirectory musicDirectory; + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + musicDirectory = musicService.getAlbum(dirId, "dir", false, downloadService, null); + } else { + musicDirectory = musicService.getMusicDirectory(dirId, "dir", false, downloadService, null); + } + + List<Entry> playEntries = new ArrayList<>(); + List<Entry> allEntries = musicDirectory.getChildren(false, true); + for(Entry song: allEntries) { + if (!song.isVideo() && song.getRating() != 1) { + playEntries.add(song); + } + } + playSongs(playEntries, shuffle, append, playFromBookmark); + + return null; + } + }.execute(); + } - private void playSong(MusicDirectory.Entry entry) { - List<MusicDirectory.Entry> entries = new ArrayList<>(); + private void playSong(Entry entry) { + + } + private void playSong(Entry entry, boolean resumeFromBookmark) { + List<Entry> entries = new ArrayList<>(); entries.add(entry); - playSongs(entries); + playSongs(entries, false, false, resumeFromBookmark); } - private void playSongs(List<MusicDirectory.Entry> entries) { + private void playSongs(List<Entry> entries) { playSongs(entries, false, false); } - private void playSongs(List<MusicDirectory.Entry> entries, boolean shuffle, boolean append) { + private void playSongs(List<Entry> entries, boolean shuffle, boolean append) { + playSongs(entries, shuffle, append, false); + } + private void playSongs(List<Entry> entries, boolean shuffle, boolean append, boolean resumeFromBookmark) { if(!append) { downloadService.clear(); } - downloadService.download(entries, false, true, false, shuffle); + + int startIndex = 0; + int startPosition = 0; + if(resumeFromBookmark) { + int bookmarkIndex = 0; + for(Entry entry: entries) { + if(entry.getBookmark() != null) { + Bookmark bookmark = entry.getBookmark(); + startIndex = bookmarkIndex; + startPosition = bookmark.getPosition(); + break; + } + bookmarkIndex++; + } + } + + downloadService.download(entries, false, !append, false, shuffle, startIndex, startPosition); } private void noResults() { @@ -415,6 +482,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { public void onPlayFromSearch (String query, Bundle extras) { // User just asked to playing something if("".equals(query)) { + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); @@ -435,6 +503,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { @@ -467,6 +536,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } } + @Override public void onPlayFromMediaId (String mediaId, Bundle extras) { if(extras == null) { return; @@ -474,11 +544,33 @@ public class RemoteControlClientLP extends RemoteControlClientBase { boolean shuffle = extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false); boolean playLast = extras.getBoolean(Constants.INTENT_EXTRA_PLAY_LAST, false); + Entry entry = (Entry) extras.getSerializable(Constants.INTENT_EXTRA_ENTRY); + String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null); if(playlistId != null) { Playlist playlist = new Playlist(playlistId, null); playPlaylist(playlist, shuffle, playLast); } + String musicDirectoryId = extras.getString(Constants.INTENT_EXTRA_NAME_ID); + if(musicDirectoryId != null) { + Entry dir = new Entry(musicDirectoryId); + playMusicDirectory(dir, shuffle, playLast, true); + } + + String podcastId = extras.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, null); + if(podcastId != null) { + playSong(entry, true); + } + + // Currently only happens when playing bookmarks so we should be looking up parent + String childId = extras.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID, null); + if(childId != null) { + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + playMusicDirectory(entry.getAlbumId(), shuffle, playLast, true); + } else { + playMusicDirectory(entry.getParent(), shuffle, playLast, true); + } + } } @Override @@ -491,5 +583,18 @@ public class RemoteControlClientLP extends RemoteControlClientBase { downloadService.toggleStarred(); } } + + @Override + public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { + if (getMediaSession() != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { + KeyEvent keyEvent = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent != null) { + downloadService.handleKeyEvent(keyEvent); + return true; + } + } + + return super.onMediaButtonEvent(mediaButtonIntent); + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java index ea61f36c..69668475 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java +++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java @@ -134,7 +134,11 @@ public class ID3v2File extends Common { if(endValue != -1) { parts.add(txData[1].substring(endName + 1, endValue)); nextStartIndex = endValue + 1; + } else { + break; } + } else { + break; } startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex); diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java index 048d5a75..3084e962 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java @@ -30,6 +30,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.Util; import java.io.File; @@ -40,6 +41,7 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { private TextView titleView; private TextView artistView; private boolean showArtist = true; + private String coverArtId; public AlbumView(Context context, boolean cell) { super(context); @@ -82,12 +84,13 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { artist += album.getYear(); } artistView.setText(album.getArtist() == null ? "" : artist); - imageTask = imageLoader.loadImage(coverArtView, album, false, true); + onUpdateImageView(); file = null; } public void onUpdateImageView() { imageTask = item2.loadImage(coverArtView, item, false, true); + coverArtId = item.getCoverArt(); } @Override @@ -101,6 +104,15 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { isRated = item.getRating(); } + @Override + public void update() { + super.update(); + + if(!Util.equals(item.getCoverArt(), coverArtId)) { + onUpdateImageView(); + } + } + public MusicDirectory.Entry getEntry() { return item; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java new file mode 100644 index 00000000..35ce71bc --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java @@ -0,0 +1,146 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + Copyright 2015 (C) Scott Jackson +*/ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.preference.DialogPreference; +import android.preference.EditTextPreference; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.File; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.FileUtil; + +public class CacheLocationPreference extends EditTextPreference { + private static final String TAG = CacheLocationPreference.class.getSimpleName(); + private Context context; + + public CacheLocationPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + public CacheLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + public CacheLocationPreference(Context context) { + super(context); + this.context = context; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setLayoutParams(new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + ViewGroup vg = (ViewGroup) editText.getParent(); + + LinearLayout cacheButtonsWrapper = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.cache_location_buttons, vg, true); + Button internalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_internal); + Button externalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_external); + + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + // Past 5.0 we can query directly for SD Card + File internalDir = null, externalDir = null; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null) { + if(Environment.isExternalStorageRemovable(dirs[i])) { + if(externalDir != null) { + externalDir = dirs[i]; + } + } else { + internalDir = dirs[i]; + } + + if(internalDir != null && externalDir != null) { + break; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + if(externalDir == null) { + for (int i = dirs.length - 1; i >= 0; i--) { + if (dirs[i] != null) { + externalDir = dirs[i]; + break; + } + } + } + if(internalDir == null) { + for (int i = 0; i < dirs.length; i++) { + if (dirs[i] != null) { + internalDir = dirs[i]; + break; + } + } + } + final File finalInternalDir = new File(internalDir, "music"); + final File finalExternalDir = new File(externalDir, "music"); + + final EditText editTextBox = (EditText)view.findViewById(android.R.id.edit); + if(finalInternalDir != null && (finalInternalDir.exists() || finalInternalDir.mkdirs())) { + internalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalInternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + internalLocation.setEnabled(false); + } + + if(finalExternalDir != null && !finalInternalDir.equals(finalExternalDir) && (finalExternalDir.exists() || finalExternalDir.mkdirs())) { + externalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalExternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + externalLocation.setEnabled(false); + } + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/CardView.java b/app/src/main/java/github/daneren2005/dsub/view/CardView.java new file mode 100644 index 00000000..d6bca330 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CardView.java @@ -0,0 +1,67 @@ +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; + +public class CardView extends FrameLayout{ + private static final String TAG = CardView.class.getSimpleName(); + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + @Override + public void onDraw(Canvas canvas) { + try { + Path clipPath = new Path(); + float roundedDp = getResources().getDimension(R.dimen.Card_Radius); + clipPath.addRoundRect(new RectF(canvas.getClipBounds()), roundedDp, roundedDp, Path.Direction.CW); + canvas.clipPath(clipPath); + } catch(Exception e) { + Log.e(TAG, "Failed to clip path on canvas", e); + } + super.onDraw(canvas); + } + + private void init(Context context) { + setClipChildren(true); + setBackgroundResource(DrawableTint.getDrawableRes(context, R.attr.cardBackgroundDrawable)); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(getResources().getInteger(R.integer.Card_Elevation)); + } + + // clipPath is not supported with Hardware Acceleration before API 18 + // http://stackoverflow.com/questions/8895677/work-around-canvas-clippath-that-is-not-supported-in-android-any-more/8895894#8895894 + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && isHardwareAccelerated()) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java index 7cb29835..d3eacefc 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java +++ b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java @@ -95,7 +95,7 @@ public class FastScroller extends LinearLayout { switch(action) { case MotionEvent.ACTION_DOWN: - if(event.getX() < (handle.getX() - 20)) { + if(event.getX() < (handle.getX() - 30)) { return false; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java index b59e7157..a33746c4 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java +++ b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java @@ -18,10 +18,17 @@ package github.daneren2005.dsub.view; import android.graphics.Rect; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import static android.widget.LinearLayout.*; public class GridSpacingDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = GridSpacingDecoration.class.getSimpleName(); public static final int SPACING = 10; @Override @@ -39,30 +46,52 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration { } int spanCount = getTotalSpan(view, parent); int spanIndex = childIndex % spanCount; + + // If we can, use the SpanSizeLookup since headers screw up the index calculation + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); + if(spanSizeLookup != null) { + spanIndex = spanSizeLookup.getSpanIndex(childIndex, spanCount); + } + } int spanSize = getSpanSize(parent, childIndex); /* INVALID SPAN */ if (spanCount < 1 || spanSize > 1) return; - outRect.top = halfSpacing; - outRect.bottom = halfSpacing; - outRect.left = halfSpacing; - outRect.right = halfSpacing; + int margins = 0; + if(view instanceof UpdateView) { + View firstChild = ((ViewGroup) view).getChildAt(0); + ViewGroup.LayoutParams layoutParams = firstChild.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + margins = ((LinearLayout.LayoutParams) layoutParams).bottomMargin; + } else if (layoutParams instanceof FrameLayout.LayoutParams) { + margins = ((FrameLayout.LayoutParams) layoutParams).bottomMargin; + } + } + int doubleMargins = margins * 2; + + outRect.top = halfSpacing - margins; + outRect.bottom = halfSpacing - margins; + outRect.left = halfSpacing - margins; + outRect.right = halfSpacing - margins; if (isTopEdge(childIndex, spanCount)) { - outRect.top = spacing; + outRect.top = spacing - doubleMargins; } if (isLeftEdge(spanIndex, spanCount)) { - outRect.left = spacing; + outRect.left = spacing - doubleMargins; } if (isRightEdge(spanIndex, spanCount)) { - outRect.right = spacing; + outRect.right = spacing - doubleMargins; } if (isBottomEdge(childIndex, childCount, spanCount)) { - outRect.bottom = spacing; + outRect.bottom = spacing - doubleMargins; } } diff --git a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java index d46dc5d2..6dc116f8 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java @@ -23,6 +23,7 @@ import android.widget.TextView; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; import static github.daneren2005.dsub.domain.User.Setting; @@ -51,12 +52,14 @@ public class SettingView extends UpdateView2<Setting, Boolean> { protected void setObjectImpl(Setting setting, Boolean isEditable) { // Can't edit non-role parts String name = setting.getName(); - if(name.indexOf("Role") == -1) { + if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) { item2 = false; } int res = -1; - if(User.SCROBBLING.equals(name)) { + if(setting instanceof MusicFolderSetting) { + titleView.setText(((MusicFolderSetting) setting).getLabel()); + } else if(User.SCROBBLING.equals(name)) { res = R.string.admin_scrobblingEnabled; } else if(User.ADMIN.equals(name)) { res = R.string.admin_role_admin; @@ -78,6 +81,8 @@ public class SettingView extends UpdateView2<Setting, Boolean> { res = R.string.admin_role_jukebox; } else if(User.SHARE.equals(name)) { res = R.string.admin_role_share; + } else if(User.VIDEO_CONVERSION.equals(name)) { + res = R.string.admin_role_video_conversion; } else if(User.LASTFM.equals(name)) { res = R.string.admin_role_lastfm; } else { diff --git a/app/src/main/java/github/daneren2005/dsub/view/SongView.java b/app/src/main/java/github/daneren2005/dsub/view/SongView.java index 625303b7..8cb0c21c 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SongView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SongView.java @@ -29,6 +29,7 @@ import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.File; @@ -41,12 +42,15 @@ import java.io.File; public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private static final String TAG = SongView.class.getSimpleName(); + private TextView trackTextView; private TextView titleTextView; + private TextView playingTextView; private TextView artistTextView; private TextView durationTextView; private TextView statusTextView; private ImageView statusImageView; private ImageView bookmarkButton; + private ImageView playedButton; private View bottomRowView; private DownloadService downloadService; @@ -63,13 +67,17 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private boolean partialFileExists = false; private boolean loaded = false; private boolean isBookmarked = false; - private boolean bookmarked = false; + private boolean isBookmarkedShown = false; private boolean showPodcast = false; + private boolean isPlayed = false; + private boolean isPlayedShown = false; + private boolean showAlbum = false; public SongView(Context context) { super(context); LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + trackTextView = (TextView) findViewById(R.id.song_track); titleTextView = (TextView) findViewById(R.id.song_title); artistTextView = (TextView) findViewById(R.id.song_artist); durationTextView = (TextView) findViewById(R.id.song_duration); @@ -80,6 +88,7 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { starButton.setFocusable(false); bookmarkButton = (ImageButton) findViewById(R.id.song_bookmark); bookmarkButton.setFocusable(false); + playedButton = (ImageButton) findViewById(R.id.song_played); moreButton = (ImageView) findViewById(R.id.item_more); bottomRowView = findViewById(R.id.song_bottom); } @@ -102,12 +111,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { if(artist.length() != 0) { artist.append(" - "); } - int index = date.indexOf(" "); - artist.append(date.substring(0, index != -1 ? index : date.length())); + artist.append(Util.formatDate(context, date, false)); } } else if(song.getArtist() != null) { - artist.append(song.getArtist()); + if(showAlbum) { + artist.append(song.getAlbum()); + } else { + artist.append(song.getArtist()); + } } if(isPodcast) { @@ -138,8 +150,23 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { String title = song.getTitle(); Integer track = song.getTrack(); + TextView newPlayingTextView; if(track != null && Util.getDisplayTrack(context)) { - title = String.format("%02d", track) + " " + title; + trackTextView.setText(String.format("%02d", track)); + trackTextView.setVisibility(View.VISIBLE); + newPlayingTextView = trackTextView; + } else { + trackTextView.setVisibility(View.GONE); + newPlayingTextView = titleTextView; + } + + if(newPlayingTextView != playingTextView || playingTextView == null) { + if(playing) { + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playing = false; + } + + playingTextView = newPlayingTextView; } titleTextView.setText(title); @@ -191,6 +218,10 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { item.loadMetadata(downloadFile.getCompleteFile()); loaded = true; } + + if(item instanceof PodcastEpisode || item.isAudioBook() || item.isPodcast()) { + isPlayed = SongDBHandler.getHandler(context).hasBeenCompleted(item); + } } @Override @@ -242,32 +273,48 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { rightImage = false; } - boolean playing = downloadService.getCurrentPlaying() == downloadFile; + boolean playing = Util.equals(downloadService.getCurrentPlaying(), downloadFile); if (playing) { if(!this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); } } else { if(this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } } if(isBookmarked) { - if(!bookmarked) { + if(!isBookmarkedShown) { if(bookmarkButton.getDrawable() == null) { bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected)); } bookmarkButton.setVisibility(View.VISIBLE); - bookmarked = true; + isBookmarkedShown = true; } } else { - if(bookmarked) { + if(isBookmarkedShown) { bookmarkButton.setVisibility(View.GONE); - bookmarked = false; + isBookmarkedShown = false; + } + } + + if(isPlayed) { + if(!isPlayedShown) { + if(playedButton.getDrawable() == null) { + playedButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_played)); + } + + playedButton.setVisibility(View.VISIBLE); + isPlayedShown = true; + } + } else { + if(isPlayedShown) { + playedButton.setVisibility(View.GONE); + isPlayedShown = false; } } @@ -288,7 +335,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { // Still highlight red if a 1-star if(isRated == 1) { this.setBackgroundColor(Color.RED); - this.getBackground().setAlpha(20); + + String theme = Util.getTheme(context); + if("black".equals(theme)) { + this.getBackground().setAlpha(80); + } else if("dark".equals(theme) || "holo".equals(theme)) { + this.getBackground().setAlpha(60); + } else { + this.getBackground().setAlpha(20); + } } else if(rating == 1) { this.setBackgroundColor(0x00000000); } @@ -304,4 +359,8 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { public void setShowPodcast(boolean showPodcast) { this.showPodcast = showPodcast; } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java index 8f3b5271..0041eac5 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java @@ -201,6 +201,10 @@ public abstract class UpdateView<T> extends LinearLayout { }); } + public static boolean hasActiveActivity() { + return activeActivities > 0; + } + public static void addActiveActivity() { activeActivities++; |