content: /*
   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 2009 (C) Sindre Mehus
   */
  package net.nullsum.audinaut.fragments;
  
  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.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 androidx.appcompat.app.AlertDialog;
  import androidx.appcompat.app.AppCompatActivity;
  import androidx.appcompat.widget.SearchView;
  import androidx.core.view.MenuItemCompat;
  import androidx.fragment.app.Fragment;
  import androidx.recyclerview.widget.GridLayoutManager;
  import androidx.recyclerview.widget.LinearLayoutManager;
  import androidx.recyclerview.widget.RecyclerView;
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  
  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(Context context) {
          super.onAttach(context);
          this.context = (SubsonicActivity) getActivity();
      }
  
      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(true);
                  return true;
              case R.id.menu_play_now:
                  playNow(false, false, false);
                  return true;
              case R.id.menu_play_last:
                  playNow(false, true, false);
                  return true;
              case R.id.menu_play_next:
                  playNow(false, true, true);
                  return true;
              case R.id.menu_shuffle:
                  playNow(true, false, 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<Entry> 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<MenuItem> 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<Entry> 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;
      }
  
      void refresh(boolean refresh) {
      }
  
      @Override
      public void onRefresh() {
          refreshLayout.setRefreshing(false);
          refresh(true);
      }
  
      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();
          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, "");
  
          genreBox.setVisibility(View.GONE);
          genreCombo.setOnClickListener(new View.OnClickListener() {
              public void onClick(View v) {
                  new LoadingTask<List<Genre>>(context, true) {
                      @Override
                      protected List<Genre> doInBackground() throws Throwable {
                          MusicService musicService = MusicServiceFactory.getMusicService(context);
                          return musicService.getGenres(false, context, this);
                      }
  
                      @Override
                      protected void done(final List<Genre> genres) {
                          List<String> 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<String> 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();
              }
          });
  
          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;
                      genre = genreCombo.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, 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<Entry> 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<Entry> songs) {
          Iterator<Entry> 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<List<Playlist>>(context, true) {
              @Override
              protected List<Playlist> doInBackground() throws Throwable {
                  MusicService musicService = MusicServiceFactory.getMusicService(context);
                  List<Playlist> playlists = new ArrayList<>(musicService.getPlaylists(false, context, this));
  
                  // Iterate through and remove all non owned public playlists
                  Iterator<Playlist> 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<Playlist> 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<Playlist>(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<Entry> songs) {
          new SilentBackgroundTask<Void>(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<Entry> 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<Entry> songs, final String name) {
          new SilentBackgroundTask<Void>(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<Entry> songs, final String name, final String id) {
          new SilentBackgroundTask<Void>(context) {
              @Override
              protected Void doInBackground() throws Throwable {
                  MusicService musicService = MusicServiceFactory.getMusicService(context);
                  MusicDirectory playlist = musicService.getPlaylist(true, id, name, context, null);
                  List<Entry> 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
                      // 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<Integer> headers = new ArrayList<>();
          List<String> 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<Void>(context) {
              @Override
              protected Void doInBackground() {
                  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(true);
                      }
                  } else {
                      UpdateView.triggerUpdate();
                  }
              }
          }.execute();
      }
  
      private void deleteSongs(final List<Entry> songs) {
          new LoadingTask<Void>(context) {
              @Override
              protected Void doInBackground() {
                  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(true);
                      }
                  } 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<Entry> entries, Entry entry) {
          onSongPress(entries, entry, true);
      }
  
      void onSongPress(List<Entry> entries, Entry entry, boolean allowPlayAll) {
          List<Entry> 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<Entry> entries) {
          playNow(entries, null, null);
      }
  
      private void playNow(final List<Entry> 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<Entry> entries, Entry song) {
          playNow(entries, song, null, null);
      }
  
      private void playNow(final List<Entry> entries, final Entry song, final String playlistName, final String playlistId) {
          new LoadingTask<Void>(context) {
              @Override
              protected Void doInBackground() {
                  playNowInTask(entries, song, playlistName, playlistId);
                  return null;
              }
  
              @Override
              protected void done(Void result) {
                  context.openNowPlaying();
              }
          }.execute();
      }
  
      private void playNowInTask(final List<Entry> 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<Entry> getSelectedEntries() {
          return getCurrentAdapter().getSelected();
      }
  
      void playNow(final boolean shuffle, final boolean append, final boolean playNext) {
          List<Entry> songs = getSelectedEntries();
          if (!songs.isEmpty()) {
              download(songs, append, !append, playNext, shuffle);
              clearSelected();
          }
      }
  
      void download(List<Entry> entries, boolean append, boolean autoplay, boolean playNext, boolean shuffle) {
          download(entries, append, autoplay, playNext, shuffle, null, null);
      }
  
      void download(final List<Entry> 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<Entry> songs = getSelectedEntries();
          if (!songs.isEmpty()) {
              downloadBackground(save, songs);
          }
      }
  
      void downloadBackground(final boolean save, final List<Entry> 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<Entry> 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<Boolean> {
          static final int MAX_SONGS = 500;
          MusicService musicService;
          boolean playNowOverride = false;
          List<Entry> songs = new ArrayList<>();
  
          public RecursiveLoader(AppCompatActivity context) {
              super(context);
              musicService = MusicServiceFactory.getMusicService(context);
          }
  
          void getSongsRecursively(List<Entry> entry) throws Exception {
              getSongsRecursively(entry, songs);
          }
  
          void getSongsRecursively(List<Entry> entry, List<Entry> songs) throws Exception {
              MusicDirectory dir = new MusicDirectory();
              dir.addChildren(entry);
              getSongsRecursively(dir, songs);
          }
  
          void getSongsRecursively(MusicDirectory parent, List<Entry> 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();
              }
          }
      }
  }
  