/* 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 . Copyright 2009 (C) Sindre Mehus */ package net.nullsum.audinaut.fragments; import android.app.Activity; import android.app.SearchManager; import android.app.SearchableInfo; import android.content.Context; import android.content.SharedPreferences; import android.media.MediaMetadataRetriever; import android.os.Bundle; import android.os.StatFs; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; 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; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import net.nullsum.audinaut.R; import net.nullsum.audinaut.activity.SubsonicActivity; import net.nullsum.audinaut.adapter.SectionAdapter; import net.nullsum.audinaut.domain.Artist; import net.nullsum.audinaut.domain.Genre; import net.nullsum.audinaut.domain.MusicDirectory; import net.nullsum.audinaut.domain.Playlist; import net.nullsum.audinaut.service.DownloadFile; import net.nullsum.audinaut.service.DownloadService; import net.nullsum.audinaut.service.MediaStoreService; import net.nullsum.audinaut.service.MusicService; import net.nullsum.audinaut.service.MusicServiceFactory; import net.nullsum.audinaut.service.OfflineException; import net.nullsum.audinaut.util.Constants; import net.nullsum.audinaut.util.FileUtil; import net.nullsum.audinaut.util.ImageLoader; import net.nullsum.audinaut.util.LoadingTask; import net.nullsum.audinaut.util.MenuUtil; import net.nullsum.audinaut.util.ProgressListener; import net.nullsum.audinaut.util.SilentBackgroundTask; import net.nullsum.audinaut.util.SongDBHandler; import net.nullsum.audinaut.util.UserUtil; import net.nullsum.audinaut.util.Util; import net.nullsum.audinaut.view.GridSpacingDecoration; import net.nullsum.audinaut.view.PlaylistSongView; import net.nullsum.audinaut.view.UpdateView; import java.io.File; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import static net.nullsum.audinaut.domain.MusicDirectory.Entry; public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = SubsonicFragment.class.getSimpleName(); private static int TAG_INC = 10; SubsonicActivity context; CharSequence title = null; CharSequence subtitle = null; View rootView; boolean primaryFragment = false; boolean secondaryFragment = false; boolean isOnlyVisible = true; boolean alwaysFullscreen = false; boolean alwaysStartFullscreen = false; boolean invalidated = false; GestureDetector gestureScanner; boolean artist = false; SwipeRefreshLayout refreshLayout; MenuItem searchItem; private boolean artistOverride = false; private boolean firstRun; private SearchView searchView; private int tag; public SubsonicFragment() { super(); tag = TAG_INC++; } @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); if (bundle != null) { String name = bundle.getString(Constants.FRAGMENT_NAME); if (name != null) { title = name; } } firstRun = true; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (title != null) { outState.putString(Constants.FRAGMENT_NAME, title.toString()); } } @Override public void onResume() { super.onResume(); if (firstRun) { firstRun = false; } else { UpdateView.triggerUpdate(); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); context = (SubsonicActivity) activity; } public void setContext(SubsonicActivity context) { this.context = context; } 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(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_refresh: refresh(); return true; case R.id.menu_play_now: playNow(false, false); return true; case R.id.menu_play_last: playNow(false, true); return true; case R.id.menu_play_next: playNow(false, true, true); return true; case R.id.menu_shuffle: playNow(true, false); return true; case R.id.menu_download: downloadBackground(false); clearSelected(); return true; case R.id.menu_cache: downloadBackground(true); clearSelected(); return true; case R.id.menu_delete: delete(); clearSelected(); return true; case R.id.menu_add_playlist: List songs = getSelectedEntries(); addToPlaylist(songs); clearSelected(); return true; } return false; } void onCreateContextMenuSupport(Menu menu, MenuInflater menuInflater, UpdateView updateView, Object selected) { if (selected instanceof Entry) { Entry entry = (Entry) selected; if (entry.isDirectory()) { if (Util.isOffline(context)) { menuInflater.inflate(R.menu.select_album_context_offline, menu); } else { menuInflater.inflate(R.menu.select_album_context, menu); } } else { if (Util.isOffline(context)) { menuInflater.inflate(R.menu.select_song_context_offline, menu); } else { menuInflater.inflate(R.menu.select_song_context, menu); String songPressAction = Util.getSongPressAction(context); if (!"next".equals(songPressAction) && !"last".equals(songPressAction)) { menu.setGroupVisible(R.id.hide_play_now, false); } } } if (!isShowArtistEnabled() || (entry.getArtistId() == null)) { menu.setGroupVisible(R.id.hide_show_artist, false); } } else if (selected instanceof Artist) { if (Util.isOffline(context)) { menuInflater.inflate(R.menu.select_artist_context_offline, menu); } else { menuInflater.inflate(R.menu.select_artist_context, menu); } } MenuUtil.hideMenuItems(context, menu, updateView); } void recreateContextMenu(Menu menu) { List menuItems = new ArrayList<>(); for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); if (item.isVisible()) { menuItems.add(item); } } menu.clear(); for (int i = 0; i < menuItems.size(); i++) { MenuItem item = menuItems.get(i); menu.add(tag, item.getItemId(), Menu.NONE, item.getTitle()); } } // For reverting specific removals: https://github.com/daneren2005/Subsonic/commit/fbd1a68042dfc3601eaa0a9e37b3957bbdd51420 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 songs = new ArrayList<>(1); songs.add(entry); switch (menuItem.getItemId()) { case R.id.artist_menu_play_now: downloadRecursively(artist.getId(), false, false, true, false, false, false); break; case R.id.artist_menu_play_shuffled: downloadRecursively(artist.getId(), false, false, true, true, false, false); break; case R.id.artist_menu_play_next: downloadRecursively(artist.getId(), false, true, false, false, false, true); break; case R.id.artist_menu_play_last: downloadRecursively(artist.getId(), false, true, false, false, false, false); break; case R.id.artist_menu_download: downloadRecursively(artist.getId(), false, true, false, false, true, false); break; case R.id.artist_menu_pin: downloadRecursively(artist.getId(), true, true, false, false, true, false); break; case R.id.artist_menu_delete: deleteRecursively(artist); break; case R.id.album_menu_play_now: artistOverride = true; downloadRecursively(entry.getId(), false, false, true, false, false, false); break; case R.id.album_menu_play_shuffled: artistOverride = true; downloadRecursively(entry.getId(), false, false, true, true, false, false); break; case R.id.album_menu_play_next: artistOverride = true; downloadRecursively(entry.getId(), false, true, false, false, false, true); break; case R.id.album_menu_play_last: artistOverride = true; downloadRecursively(entry.getId(), false, true, false, false, false, false); break; case R.id.album_menu_download: artistOverride = true; downloadRecursively(entry.getId(), false, true, false, false, true, false); break; case R.id.album_menu_pin: artistOverride = true; downloadRecursively(entry.getId(), true, true, false, false, true, false); break; case R.id.album_menu_delete: deleteRecursively(entry); break; case R.id.album_menu_info: displaySongInfo(entry); break; case R.id.album_menu_show_artist: showAlbumArtist((Entry) selectedItem); break; case R.id.song_menu_play_now: playNow(songs); break; case R.id.song_menu_play_next: getDownloadService().download(songs, false, false, true, false); break; case R.id.song_menu_play_last: getDownloadService().download(songs, false, false, false, false); break; case R.id.song_menu_download: getDownloadService().downloadBackground(songs, false); break; case R.id.song_menu_pin: getDownloadService().downloadBackground(songs, true); break; case R.id.song_menu_delete: deleteSongs(songs); break; case R.id.song_menu_add_playlist: addToPlaylist(songs); break; case R.id.song_menu_info: displaySongInfo(entry); break; case R.id.song_menu_show_album: showAlbum((Entry) selectedItem); break; case R.id.song_menu_show_artist: showArtist((Entry) selectedItem); break; default: return false; } return true; } void replaceFragment(SubsonicFragment fragment) { context.replaceFragment(fragment, fragment.getSupportTag(), secondaryFragment); } void removeCurrent() { context.removeCurrent(); } public int getRootId() { return rootView.getId(); } public int getSupportTag() { return tag; } public void setSupportTag(String tag) { this.tag = Integer.parseInt(tag); } public void setPrimaryFragment(boolean primary) { primaryFragment = primary; if (primary) { if (context != null && title != null) { context.setTitle(title); context.setSubtitle(subtitle); } if (invalidated) { invalidated = false; refresh(false); } } } public void setPrimaryFragment(boolean primary, boolean secondary) { setPrimaryFragment(primary); secondaryFragment = secondary; } public void setSecondaryFragment(boolean secondary) { secondaryFragment = secondary; } public void setIsOnlyVisible(boolean isOnlyVisible) { this.isOnlyVisible = isOnlyVisible; } public boolean isAlwaysFullscreen() { return alwaysFullscreen; } public boolean isAlwaysStartFullscreen() { return alwaysStartFullscreen; } public void invalidate() { if (primaryFragment) { refresh(true); } else { invalidated = true; } } public DownloadService getDownloadService() { return context != null ? context.getDownloadService() : null; } private void refresh() { refresh(true); } void refresh(boolean refresh) { } @Override public void onRefresh() { refreshLayout.setRefreshing(false); refresh(); } public void setProgressVisible(boolean visible) { View view = rootView.findViewById(R.id.tab_progress); if (view != null) { view.setVisibility(visible ? View.VISIBLE : View.GONE); if (visible) { View progress = rootView.findViewById(R.id.tab_progress_spinner); progress.setVisibility(View.VISIBLE); } } } public void updateProgress(String message) { TextView view = rootView.findViewById(R.id.tab_progress_message); if (view != null) { view.setText(message); } } void setEmpty(boolean empty) { View view = rootView.findViewById(R.id.tab_progress); if (empty) { view.setVisibility(View.VISIBLE); View progress = view.findViewById(R.id.tab_progress_spinner); progress.setVisibility(View.GONE); TextView text = view.findViewById(R.id.tab_progress_message); text.setText(R.string.common_empty); } else { view.setVisibility(View.GONE); } } synchronized ImageLoader getImageLoader() { return context.getImageLoader(); } void setTitle(CharSequence title) { this.title = title; context.setTitle(title); } void setSubtitle(CharSequence title) { this.subtitle = title; context.setSubtitle(title); } public CharSequence getTitle() { return this.title; } void setTitle(int title) { this.title = context.getResources().getString(title); context.setTitle(this.title); } void setupScrollList(final RecyclerView recyclerView) { if (!context.isTouchscreen()) { refreshLayout.setEnabled(false); } else { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { refreshLayout.setEnabled(!recyclerView.canScrollVertically(-1)); } }); } } void setupLayoutManager(RecyclerView recyclerView, boolean largeAlbums) { recyclerView.setLayoutManager(getLayoutManager(recyclerView, largeAlbums)); } private RecyclerView.LayoutManager getLayoutManager(RecyclerView recyclerView, boolean largeCells) { if (largeCells) { return getGridLayoutManager(recyclerView); } else { return getLinearLayoutManager(); } } private GridLayoutManager getGridLayoutManager(RecyclerView recyclerView) { final int columns = getRecyclerColumnCount(); GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); if (spanSizeLookup != null) { gridLayoutManager.setSpanSizeLookup(spanSizeLookup); } RecyclerView.ItemDecoration itemDecoration = getItemDecoration(); if (itemDecoration != null) { recyclerView.addItemDecoration(itemDecoration); } return gridLayoutManager; } private LinearLayoutManager getLinearLayoutManager() { LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); return layoutManager; } GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { SectionAdapter adapter = getCurrentAdapter(); if (adapter != null) { int viewType = adapter.getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { return gridLayoutManager.getSpanCount(); } else { return 1; } } else { return 1; } } }; } private RecyclerView.ItemDecoration getItemDecoration() { return new GridSpacingDecoration(); } int getRecyclerColumnCount() { if (isOnlyVisible) { return context.getResources().getInteger(R.integer.Grid_FullScreen_Columns); } else { return context.getResources().getInteger(R.integer.Grid_Columns); } } void warnIfStorageUnavailable() { if (!Util.isExternalStoragePresent()) { Util.toast(context, R.string.select_album_no_sdcard); } try { StatFs stat = new StatFs(FileUtil.getMusicDirectory(context).getPath()); long bytesAvailableFs = stat.getAvailableBlocksLong() * stat.getBlockSizeLong(); if (bytesAvailableFs < 50000000L) { Util.toast(context, context.getResources().getString(R.string.select_album_no_room, Util.formatBytes(bytesAvailableFs))); } } catch (Exception e) { Log.w(TAG, "Error while checking storage space for music directory", e); } } private void onShuffleRequested() { if (Util.isOffline(context)) { DownloadService downloadService = getDownloadService(); if (downloadService == null) { return; } downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); return; } View dialogView = context.getLayoutInflater().inflate(R.layout.shuffle_dialog, null); final EditText startYearBox = dialogView.findViewById(R.id.start_year); final EditText endYearBox = dialogView.findViewById(R.id.end_year); final EditText genreBox = dialogView.findViewById(R.id.genre); final Button genreCombo = dialogView.findViewById(R.id.genre_combo); final SharedPreferences prefs = Util.getPreferences(context); final String oldStartYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""); final String oldEndYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, ""); final String oldGenre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""); boolean _useCombo; genreBox.setVisibility(View.GONE); genreCombo.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { new LoadingTask>(context, true) { @Override protected List doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); return musicService.getGenres(false, context, this); } @Override protected void done(final List genres) { List names = new ArrayList<>(); String blank = context.getResources().getString(R.string.select_genre_blank); names.add(blank); for (Genre genre : genres) { names.add(genre.getName()); } final List finalNames = names; AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.shuffle_pick_genre) .setItems(names.toArray(new CharSequence[names.size()]), (dialog, which) -> { if (which == 0) { genreCombo.setText(""); } else { genreCombo.setText(finalNames.get(which)); } }); AlertDialog dialog = builder.create(); dialog.show(); } @Override protected void error(Throwable error) { String msg; if (error instanceof OfflineException) { msg = getErrorMessage(error); } else { msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); } Util.toast(context, msg, false); } }.execute(); } }); _useCombo = true; final boolean useCombo = _useCombo; startYearBox.setText(oldStartYear); endYearBox.setText(oldEndYear); genreBox.setText(oldGenre); genreCombo.setText(oldGenre); AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.shuffle_title) .setView(dialogView) .setPositiveButton(R.string.common_ok, (dialog, id) -> { String genre; if (useCombo) { genre = genreCombo.getText().toString(); } else { genre = genreBox.getText().toString(); } String startYear = startYearBox.getText().toString(); String endYear = endYearBox.getText().toString(); SharedPreferences.Editor editor = prefs.edit(); editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear); editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear); editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); editor.apply(); DownloadService downloadService = getDownloadService(); if (downloadService == null) { return; } downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); }) .setNegativeButton(R.string.common_cancel, null); AlertDialog dialog = builder.create(); dialog.show(); } void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) { downloadRecursively(id, "", true, save, append, autoplay, shuffle, background, playNext); } void downloadPlaylist(final String id, final String name) { downloadRecursively(id, name); } private void downloadRecursively(final String id, final String name) { downloadRecursively(id, name, false, false, true, false, false, true, false); } private void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) { new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { musicService = MusicServiceFactory.getMusicService(context); MusicDirectory root; if (isDirectory && id != null) { root = getMusicDirectory(id, name, false, musicService, this); } else { root = musicService.getPlaylist(true, id, name, context, this); } boolean shuffleByAlbum = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_BY_ALBUM, true); if (shuffle && shuffleByAlbum) { Collections.shuffle(root.getChildren()); } songs = new LinkedList<>(); getSongsRecursively(root, songs); if (shuffle && !shuffleByAlbum) { Collections.shuffle(songs); } DownloadService downloadService = getDownloadService(); boolean transition = false; if (!songs.isEmpty() && downloadService != null) { // Conditions for a standard play now operation if (!append && !save && autoplay && !playNext && !shuffle && !background) { playNowOverride = true; return false; } if (!append && !background) { downloadService.clear(); } if (!background) { downloadService.download(songs, save, autoplay, playNext, false); if (!append) { transition = true; } } else { downloadService.downloadBackground(songs, save); } } artistOverride = false; return transition; } }.execute(); } void downloadRecursively(final List albums, final boolean shuffle, final boolean append, final boolean playNext) { new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { musicService = MusicServiceFactory.getMusicService(context); if (shuffle) { Collections.shuffle(albums); } songs = new LinkedList<>(); MusicDirectory root = new MusicDirectory(); root.addChildren(albums); getSongsRecursively(root, songs); DownloadService downloadService = getDownloadService(); boolean transition = false; if (!songs.isEmpty() && downloadService != null) { // Conditions for a standard play now operation if (!append && !shuffle) { playNowOverride = true; return false; } if (!append) { downloadService.clear(); } downloadService.download(songs, false, true, playNext, false); if (!append) { transition = true; } } artistOverride = false; return transition; } }.execute(); } MusicDirectory getMusicDirectory(String id, String name, boolean refresh, MusicService service, ProgressListener listener) throws Exception { return getMusicDirectory(id, name, refresh, false, service, listener); } MusicDirectory getMusicDirectory(String id, String name, boolean refresh, boolean forceArtist, MusicService service, ProgressListener listener) throws Exception { if (!Util.isOffline(context)) { if (artist && !artistOverride || forceArtist) { return service.getArtist(id, name, refresh, context, listener); } else { return service.getAlbum(id, name, refresh, context, listener); } } else { return service.getMusicDirectory(id, name, refresh, context, listener); } } private void addToPlaylist(final List songs) { Iterator it = songs.iterator(); while (it.hasNext()) { Entry entry = it.next(); if (entry.isDirectory()) { it.remove(); } } if (songs.isEmpty()) { Util.toast(context, "No songs selected"); return; } new LoadingTask>(context, true) { @Override protected List doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); List playlists = new ArrayList<>(); playlists.addAll(musicService.getPlaylists(false, context, this)); // Iterate through and remove all non owned public playlists Iterator it = playlists.iterator(); while (it.hasNext()) { Playlist playlist = it.next(); if (playlist.getPublic() && !playlist.getId().contains(".m3u") && !UserUtil.getCurrentUsername(context).equals(playlist.getOwner())) { it.remove(); } } return playlists; } @Override protected void done(final List playlists) { // Create adapter to show playlists Playlist createNew = new Playlist("-1", context.getResources().getString(R.string.playlist_create_new)); playlists.add(0, createNew); ArrayAdapter playlistAdapter = new ArrayAdapter(context, R.layout.basic_count_item, playlists) { @Override public View getView(int position, View convertView, ViewGroup parent) { Playlist playlist = getItem(position); // Create new if not getting a convert view to use PlaylistSongView view; if (convertView instanceof PlaylistSongView) { view = (PlaylistSongView) convertView; } else { view = new PlaylistSongView(context); } view.setObject(playlist, songs); return view; } }; AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.playlist_add_to) .setAdapter(playlistAdapter, (dialog, which) -> { if (which > 0) { addToPlaylist(playlists.get(which), songs); } else { createNewPlaylist(songs, false); } }); AlertDialog dialog = builder.create(); dialog.show(); } @Override protected void error(Throwable error) { String msg; if (error instanceof OfflineException) { msg = getErrorMessage(error); } else { msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); } Util.toast(context, msg, false); } }.execute(); } private void addToPlaylist(final Playlist playlist, final List songs) { new SilentBackgroundTask(context) { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.addToPlaylist(playlist.getId(), songs, context, null); return null; } @Override protected void done(Void result) { Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName())); } @Override protected void error(Throwable error) { String msg; if (error instanceof OfflineException) { msg = getErrorMessage(error); } else { msg = context.getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error); } Util.toast(context, msg, false); } }.execute(); } void createNewPlaylist(final List songs, final boolean getSuggestion) { View layout = context.getLayoutInflater().inflate(R.layout.save_playlist, null); final EditText playlistNameView = layout.findViewById(R.id.save_playlist_name); final CheckBox overwriteCheckBox = layout.findViewById(R.id.save_playlist_overwrite); if (getSuggestion) { String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null; if (playlistName != null) { playlistNameView.setText(playlistName); try { if (Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) { overwriteCheckBox.setChecked(true); overwriteCheckBox.setVisibility(View.VISIBLE); } } catch (Exception e) { Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet"); } } else { DateFormat dateFormat = DateFormat.getDateInstance(); playlistNameView.setText(dateFormat.format(new Date())); } } else { DateFormat dateFormat = DateFormat.getDateInstance(); playlistNameView.setText(dateFormat.format(new Date())); } AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.download_playlist_title) .setMessage(R.string.download_playlist_name) .setView(layout) .setPositiveButton(R.string.common_save, (dialog, id) -> { String playlistName = String.valueOf(playlistNameView.getText()); if (overwriteCheckBox.isChecked()) { overwritePlaylist(songs, playlistName, getDownloadService().getSuggestedPlaylistId()); } else { createNewPlaylist(songs, playlistName); if (getSuggestion) { DownloadService downloadService = getDownloadService(); if (downloadService != null) { downloadService.setSuggestedPlaylistName(playlistName, null); } } } }) .setNegativeButton(R.string.common_cancel, (dialog, id) -> dialog.cancel()) .setCancelable(true); AlertDialog dialog = builder.create(); dialog.show(); } private void createNewPlaylist(final List songs, final String name) { new SilentBackgroundTask(context) { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.createPlaylist(null, name, songs, context, null); return null; } @Override protected void done(Void result) { Util.toast(context, R.string.download_playlist_done); } @Override protected void error(Throwable error) { String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); Util.toast(context, msg); } }.execute(); } private void overwritePlaylist(final List songs, final String name, final String id) { new SilentBackgroundTask(context) { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); MusicDirectory playlist = musicService.getPlaylist(true, id, name, context, null); List toDelete = playlist.getChildren(); musicService.overwritePlaylist(id, name, toDelete.size(), songs, context, null); return null; } @Override protected void done(Void result) { Util.toast(context, R.string.download_playlist_done); } @Override protected void error(Throwable error) { String msg; if (error instanceof OfflineException) { msg = getErrorMessage(error); } else { msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); } Util.toast(context, msg, false); } }.execute(); } void displaySongInfo(final Entry song) { Integer duration = null; Integer bitrate = null; String format = null; long size = 0; if (!song.isDirectory()) { try { DownloadFile downloadFile = new DownloadFile(context, song, false); File file = downloadFile.getCompleteFile(); if (file.exists()) { MediaMetadataRetriever metadata = new MediaMetadataRetriever(); metadata.setDataSource(file.getAbsolutePath()); String tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); duration = Integer.parseInt((tmp != null) ? tmp : "0") / 1000; format = FileUtil.getExtension(file.getName()); size = file.length(); // If no duration try to read bitrate tag if (duration == null) { tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE); bitrate = Integer.parseInt((tmp != null) ? tmp : "0") / 1000; } else { // Otherwise do a calculation for it // Divide by 1000 so in kbps bitrate = (int) (size / duration) / 1000 * 8; } if (Util.isOffline(context)) { song.setGenre(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)); String year = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR); song.setYear(Integer.parseInt((year != null) ? year : "0")); } } } catch (Exception e) { Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver"); } } if (duration == null) { duration = song.getDuration(); } List headers = new ArrayList<>(); List details = new ArrayList<>(); if (!song.isDirectory()) { headers.add(R.string.details_title); details.add(song.getTitle()); } if (song.getArtist() != null && !"".equals(song.getArtist())) { headers.add(R.string.details_artist); details.add(song.getArtist()); } if (song.getAlbum() != null && !"".equals(song.getAlbum())) { headers.add(R.string.details_album); details.add(song.getAlbum()); } if (song.getTrack() != null && song.getTrack() != 0) { headers.add(R.string.details_track); details.add(Integer.toString(song.getTrack())); } if (song.getGenre() != null && !"".equals(song.getGenre())) { headers.add(R.string.details_genre); details.add(song.getGenre()); } if (song.getYear() != null && song.getYear() != 0) { headers.add(R.string.details_year); details.add(Integer.toString(song.getYear())); } if (!Util.isOffline(context) && song.getSuffix() != null) { headers.add(R.string.details_server_format); details.add(song.getSuffix()); if (song.getBitRate() != null && song.getBitRate() != 0) { headers.add(R.string.details_server_bitrate); details.add(song.getBitRate() + " kbps"); } } if (format != null && !"".equals(format)) { headers.add(R.string.details_cached_format); details.add(format); } if (bitrate != null && bitrate != 0) { headers.add(R.string.details_cached_bitrate); details.add(bitrate + " kbps"); } if (size != 0) { headers.add(R.string.details_size); details.add(Util.formatLocalizedBytes(size, context)); } if (duration != null && duration != 0) { headers.add(R.string.details_length); details.add(Util.formatDuration(duration)); } try { Long[] dates = SongDBHandler.getHandler(context).getLastPlayed(song); if (dates != null && dates[0] != null && dates[0] > 0) { headers.add(R.string.details_last_played); Date date = new Date((dates[1] != null && dates[1] > dates[0]) ? dates[1] : dates[0]); DateFormat dateFormat = DateFormat.getDateInstance(); details.add(dateFormat.format(date)); } } catch (Exception e) { Log.e(TAG, "Failed to get last played", e); } int title; if (song.isDirectory()) { title = R.string.details_title_album; } else { title = R.string.details_title_song; } Util.showDetailsDialog(context, title, headers, details); } private void deleteRecursively(Artist artist) { deleteRecursively(artist, FileUtil.getArtistDirectory(context, artist)); } void deleteRecursively(Entry album) { deleteRecursively(album, FileUtil.getAlbumDirectory(context, album)); } private void deleteRecursively(final Object remove, final File dir) { if (dir == null) { return; } new LoadingTask(context) { @Override protected Void doInBackground() throws Throwable { MediaStoreService mediaStore = new MediaStoreService(context); FileUtil.recursiveDelete(dir, mediaStore); return null; } @Override protected void done(Void result) { if (Util.isOffline(context)) { SectionAdapter adapter = getCurrentAdapter(); if (adapter != null) { adapter.removeItem(remove); } else { refresh(); } } else { UpdateView.triggerUpdate(); } } }.execute(); } private void deleteSongs(final List songs) { new LoadingTask(context) { @Override protected Void doInBackground() throws Throwable { getDownloadService().delete(songs); return null; } @Override protected void done(Void result) { if (Util.isOffline(context)) { SectionAdapter adapter = getCurrentAdapter(); if (adapter != null) { for (Entry song : songs) { adapter.removeItem(song); } } else { refresh(); } } else { UpdateView.triggerUpdate(); } } }.execute(); } private void showAlbumArtist(Entry entry) { SubsonicFragment fragment = new SelectDirectoryFragment(); Bundle args = new Bundle(); args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId()); args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); fragment.setArguments(args); replaceFragment(fragment); } private void showArtist(Entry entry) { SubsonicFragment fragment = new SelectDirectoryFragment(); Bundle args = new Bundle(); args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId()); args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); fragment.setArguments(args); replaceFragment(fragment); } private void showAlbum(Entry entry) { SubsonicFragment fragment = new SelectDirectoryFragment(); Bundle args = new Bundle(); args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getAlbumId()); args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum()); fragment.setArguments(args); replaceFragment(fragment); } public GestureDetector getGestureDetector() { return gestureScanner; } void onSongPress(List entries, Entry entry) { onSongPress(entries, entry, true); } void onSongPress(List entries, Entry entry, boolean allowPlayAll) { List songs = new ArrayList<>(); String songPressAction = Util.getSongPressAction(context); if ("all".equals(songPressAction) && allowPlayAll) { for (Entry song : entries) { if (!song.isDirectory()) { songs.add(song); } } playNow(songs, entry); } else if ("next".equals(songPressAction)) { getDownloadService().download(Collections.singletonList(entry), false, false, true, false); } else if ("last".equals(songPressAction)) { getDownloadService().download(Collections.singletonList(entry), false, false, false, false); } else { songs.add(entry); playNow(songs); } } private void playNow(List entries) { playNow(entries, null, null); } private void playNow(final List entries, final String playlistName, final String playlistId) { new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { getSongsRecursively(entries, songs); return null; } @Override protected void done(Boolean result) { Entry selected = songs.isEmpty() ? null : songs.get(0); playNow(songs, selected, playlistName, playlistId); } }.execute(); } private void playNow(List entries, Entry song) { playNow(entries, song, null, null); } private void playNow(final List entries, final Entry song, final String playlistName, final String playlistId) { new LoadingTask(context) { @Override protected Void doInBackground() throws Throwable { playNowInTask(entries, song, playlistName, playlistId); return null; } @Override protected void done(Void result) { context.openNowPlaying(); } }.execute(); } private void playNowInTask(final List entries, final Entry song, 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)); downloadService.setSuggestedPlaylistName(playlistName, playlistId); } public SectionAdapter getCurrentAdapter() { return null; } public void stopActionMode() { SectionAdapter adapter = getCurrentAdapter(); if (adapter != null) { adapter.stopActionMode(); } } private void clearSelected() { if (getCurrentAdapter() != null) { getCurrentAdapter().clearSelected(); } } List getSelectedEntries() { return getCurrentAdapter().getSelected(); } private void playNow(final boolean shuffle, final boolean append) { playNow(shuffle, append, false); } void playNow(final boolean shuffle, final boolean append, final boolean playNext) { List songs = getSelectedEntries(); if (!songs.isEmpty()) { download(songs, append, !append, playNext, shuffle); clearSelected(); } } void download(List entries, boolean append, boolean autoplay, boolean playNext, boolean shuffle) { download(entries, append, autoplay, playNext, shuffle, null, null); } void download(final List entries, final boolean append, final boolean autoplay, final boolean playNext, final boolean shuffle, final String playlistName, final String playlistId) { final DownloadService downloadService = getDownloadService(); if (downloadService == null) { return; } warnIfStorageUnavailable(); // Conditions for using play now button if (!append && autoplay && !playNext && !shuffle) { // Call playNow which goes through and tries to use information playNow(entries, playlistName, playlistId); return; } RecursiveLoader onValid = new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { if (!append) { getDownloadService().clear(); } getSongsRecursively(entries, songs); downloadService.download(songs, false, autoplay, playNext, shuffle); if (playlistName != null) { downloadService.setSuggestedPlaylistName(playlistName, playlistId); } else { downloadService.setSuggestedPlaylistName(null, null); } return null; } @Override protected void done(Boolean result) { if (autoplay) { context.openNowPlaying(); } else if (append) { Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size())); } } }; executeOnValid(onValid); } private void executeOnValid(RecursiveLoader onValid) { onValid.execute(); } void downloadBackground(final boolean save) { List songs = getSelectedEntries(); if (!songs.isEmpty()) { downloadBackground(save, songs); } } void downloadBackground(final boolean save, final List entries) { if (getDownloadService() == null) { return; } warnIfStorageUnavailable(); new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { getSongsRecursively(entries); getDownloadService().downloadBackground(songs, save); return null; } @Override protected void done(Boolean result) { Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); } }.execute(); } void delete() { List songs = getSelectedEntries(); if (!songs.isEmpty()) { DownloadService downloadService = getDownloadService(); if (downloadService != null) { downloadService.delete(songs); } } } boolean isShowArtistEnabled() { return false; } String getCurrentQuery() { return null; } public abstract class RecursiveLoader extends LoadingTask { static final int MAX_SONGS = 500; MusicService musicService; boolean playNowOverride = false; List songs = new ArrayList<>(); public RecursiveLoader(Activity context) { super(context); musicService = MusicServiceFactory.getMusicService(context); } void getSongsRecursively(List entry) throws Exception { getSongsRecursively(entry, songs); } void getSongsRecursively(List entry, List songs) throws Exception { MusicDirectory dir = new MusicDirectory(); dir.addChildren(entry); getSongsRecursively(dir, songs); } void getSongsRecursively(MusicDirectory parent, List songs) throws Exception { if (songs.size() > MAX_SONGS) { return; } for (Entry dir : parent.getChildren(true, false)) { MusicDirectory musicDirectory; if (!Util.isOffline(context)) { musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this); } else { musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this); } getSongsRecursively(musicDirectory, songs); } songs.addAll(parent.getChildren(false, true)); } @Override protected void done(Boolean result) { warnIfStorageUnavailable(); if (playNowOverride) { playNow(songs); return; } if (result) { context.openNowPlaying(); } } } }