diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt index 194408e6..37bd863f 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt @@ -1,7 +1,19 @@ package org.moire.ultrasonic.domain abstract class GenericEntry { - // TODO Should be non-null! + // TODO: Should be non-null! abstract val id: String? open val name: String? = null + + // These are just a formality and will never be called, + // because Kotlin data classes will have autogenerated equals() and hashCode() functions + override operator fun equals(other: Any?): Boolean { + return this === other + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + (name?.hashCode() ?: 0) + return result + } } diff --git a/detekt-config.yml b/detekt-config.yml index 47707e69..7fff31c3 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -35,6 +35,9 @@ exceptions: empty-blocks: active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: true complexity: active: true diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index a0874cbb..037c3e49 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -183,21 +183,10 @@ errorLine2=" ^"> - - - - @@ -216,7 +205,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -227,7 +216,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -238,7 +227,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -249,7 +238,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -260,7 +249,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -271,7 +260,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -282,7 +271,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -293,7 +282,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -304,7 +293,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -315,7 +304,7 @@ errorLine2=" ^"> @@ -326,7 +315,7 @@ errorLine2=" ^"> @@ -337,7 +326,7 @@ errorLine2=" ^"> @@ -348,7 +337,7 @@ errorLine2=" ^"> @@ -359,7 +348,7 @@ errorLine2=" ^"> @@ -370,7 +359,7 @@ errorLine2=" ^"> @@ -381,7 +370,7 @@ errorLine2=" ^"> @@ -491,17 +480,6 @@ column="19"/> - - - - - + - + - + - + - + - + - + - + @@ -1219,7 +1197,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1230,27 +1208,27 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + - + - + - + @@ -1469,43 +1447,43 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1571,7 +1549,7 @@ errorLine2=" ^"> @@ -1582,7 +1560,7 @@ errorLine2=" ^"> @@ -1593,7 +1571,7 @@ errorLine2=" ^"> @@ -1604,7 +1582,7 @@ errorLine2=" ^"> @@ -1615,7 +1593,7 @@ errorLine2=" ^"> @@ -1626,7 +1604,7 @@ errorLine2=" ^"> @@ -1637,18 +1615,7 @@ errorLine2=" ^"> - - - - @@ -1659,7 +1626,7 @@ errorLine2=" ^"> @@ -1670,7 +1637,7 @@ errorLine2=" ^"> @@ -1782,61 +1749,6 @@ column="42"/> - - - - - - - - - - - - - - - - - - - - onProgressChangedTask; - LinearLayout visualizerViewLayout; - private MenuItem starMenuItem; - private ImageView fiveStar1ImageView; - private ImageView fiveStar2ImageView; - private ImageView fiveStar3ImageView; - private ImageView fiveStar4ImageView; - private ImageView fiveStar5ImageView; - private boolean useFiveStarRating; - private Drawable hollowStar; - private Drawable fullStar; - private CancellationToken cancellationToken; - - private boolean isEqualizerAvailable; - private boolean isVisualizerAvailable; - - private final Lazy networkAndStorageChecker = inject(NetworkAndStorageChecker.class); - private final Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Lazy shareHandler = inject(ShareHandler.class); - private final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - Util.applyTheme(this.getContext()); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.current_playing, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - cancellationToken = new CancellationToken(); - FragmentTitle.Companion.setTitle(this, R.string.common_appname); - - final WindowManager windowManager = getActivity().getWindowManager(); - final Display display = windowManager.getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - int width = size.x; - int height = size.y; - - setHasOptionsMenu(true); - - FeatureStorage features = KoinJavaComponent.get(FeatureStorage.class); - useFiveStarRating = features.isFeatureEnabled(Feature.FIVE_STAR_RATING); - - swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; - swipeVelocity = swipeDistance; - gestureScanner = new GestureDetector(getContext(), this); - - playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper); - emptyTextView = view.findViewById(R.id.playlist_empty); - songTitleTextView = view.findViewById(R.id.current_playing_song); - albumTextView = view.findViewById(R.id.current_playing_album); - artistTextView = view.findViewById(R.id.current_playing_artist); - albumArtImageView = view.findViewById(R.id.current_playing_album_art_image); - positionTextView = view.findViewById(R.id.current_playing_position); - downloadTrackTextView = view.findViewById(R.id.current_playing_track); - downloadTotalDurationTextView = view.findViewById(R.id.current_total_duration); - durationTextView = view.findViewById(R.id.current_playing_duration); - progressBar = view.findViewById(R.id.current_playing_progress_bar); - playlistView = view.findViewById(R.id.playlist_view); - final AutoRepeatButton previousButton = view.findViewById(R.id.button_previous); - final AutoRepeatButton nextButton = view.findViewById(R.id.button_next); - pauseButton = view.findViewById(R.id.button_pause); - stopButton = view.findViewById(R.id.button_stop); - startButton = view.findViewById(R.id.button_start); - final View shuffleButton = view.findViewById(R.id.button_shuffle); - repeatButton = view.findViewById(R.id.button_repeat); - - visualizerViewLayout = view.findViewById(R.id.current_playing_visualizer_layout); - - LinearLayout ratingLinearLayout = view.findViewById(R.id.song_rating); - fiveStar1ImageView = view.findViewById(R.id.song_five_star_1); - fiveStar2ImageView = view.findViewById(R.id.song_five_star_2); - fiveStar3ImageView = view.findViewById(R.id.song_five_star_3); - fiveStar4ImageView = view.findViewById(R.id.song_five_star_4); - fiveStar5ImageView = view.findViewById(R.id.song_five_star_5); - - if (!useFiveStarRating) ratingLinearLayout.setVisibility(View.GONE); - - hollowStar = Util.getDrawableFromAttribute(view.getContext(), R.attr.star_hollow); - fullStar = Util.getDrawableFromAttribute(getContext(), R.attr.star_full); - - fiveStar1ImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) { - setSongRating(1); - } - }); - fiveStar2ImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) { - setSongRating(2); - } - }); - fiveStar3ImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) { - setSongRating(3); - } - }); - fiveStar4ImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) { - setSongRating(4); - } - }); - fiveStar5ImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) { - setSongRating(5); - } - }); - - albumArtImageView.setOnTouchListener(new View.OnTouchListener() - { - @Override - public boolean onTouch(View view, MotionEvent me) - { - return gestureScanner.onTouchEvent(me); - } - }); - - albumArtImageView.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - toggleFullScreenAlbumArt(); - } - }); - - previousButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - mediaPlayerControllerLazy.getValue().previous(); - return null; - } - - @Override - protected void done(final Void result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - }.execute(); - } - }); - - previousButton.setOnRepeatListener(new Runnable() - { - @Override - public void run() - { - int incrementTime = Util.getIncrementTime(); - changeProgress(-incrementTime); - } - }); - - nextButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - - new SilentBackgroundTask(getActivity()) - { - @Override - protected Boolean doInBackground() - { - mediaPlayerControllerLazy.getValue().next(); - return true; - } - - @Override - protected void done(final Boolean result) - { - if (result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - } - }.execute(); - } - }); - - nextButton.setOnRepeatListener(new Runnable() - { - @Override - public void run() - { - int incrementTime = Util.getIncrementTime(); - changeProgress(incrementTime); - } - }); - - pauseButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - mediaPlayerControllerLazy.getValue().pause(); - return null; - } - - @Override - protected void done(final Void result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - }.execute(); - } - }); - - stopButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - mediaPlayerControllerLazy.getValue().reset(); - return null; - } - - @Override - protected void done(final Void result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - }.execute(); - } - }); - - startButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - start(); - return null; - } - - @Override - protected void done(final Void result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - }.execute(); - } - }); - - shuffleButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - mediaPlayerControllerLazy.getValue().shuffle(); - Util.toast(getActivity(), R.string.download_menu_shuffle_notification); - } - }); - - repeatButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(final View view) - { - final RepeatMode repeatMode = mediaPlayerControllerLazy.getValue().getRepeatMode().next(); - - mediaPlayerControllerLazy.getValue().setRepeatMode(repeatMode); - onDownloadListChanged(); - - switch (repeatMode) - { - case OFF: - Util.toast(getContext(), R.string.download_repeat_off); - break; - case ALL: - Util.toast(getContext(), R.string.download_repeat_all); - break; - case SINGLE: - Util.toast(getContext(), R.string.download_repeat_single); - break; - default: - break; - } - } - }); - - progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() - { - @Override - public void onStopTrackingTouch(final SeekBar seekBar) - { - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - mediaPlayerControllerLazy.getValue().seekTo(getProgressBar().getProgress()); - return null; - } - - @Override - protected void done(final Void result) - { - onSliderProgressChanged(); - } - }.execute(); - } - - @Override - public void onStartTrackingTouch(final SeekBar seekBar) - { - } - - @Override - public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) - { - } - }); - - playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() - { - @Override - public void onItemClick(final AdapterView parent, final View view, final int position, final long id) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() - { - mediaPlayerControllerLazy.getValue().play(position); - return null; - } - - @Override - protected void done(final Void result) - { - onCurrentChanged(); - onSliderProgressChanged(); - } - }.execute(); - } - }); - - registerForContextMenu(playlistView); - - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if (mediaPlayerController != null && getArguments() != null && getArguments().getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.setShufflePlayEnabled(true); - } - - visualizerViewLayout.setVisibility(View.GONE); - VisualizerController.get().observe(getActivity(), new Observer() { - @Override - public void onChanged(VisualizerController visualizerController) { - if (visualizerController != null) { - Timber.d("VisualizerController Observer.onChanged received controller"); - visualizerView = new VisualizerView(getContext()); - visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); - - if (!visualizerView.isActive()) - { - visualizerViewLayout.setVisibility(View.GONE); - } - else - { - visualizerViewLayout.setVisibility(View.VISIBLE); - } - - visualizerView.setOnTouchListener(new View.OnTouchListener() - { - @Override - public boolean onTouch(final View view, final MotionEvent motionEvent) - { - visualizerView.setActive(!visualizerView.isActive()); - mediaPlayerControllerLazy.getValue().setShowVisualization(visualizerView.isActive()); - return true; - } - }); - isVisualizerAvailable = true; - } else { - Timber.d("VisualizerController Observer.onChanged has no controller"); - visualizerViewLayout.setVisibility(View.GONE); - isVisualizerAvailable = false; - } - } - }); - - EqualizerController.get().observe(getActivity(), new Observer() { - @Override - public void onChanged(EqualizerController equalizerController) { - if (equalizerController != null) { - Timber.d("EqualizerController Observer.onChanged received controller"); - isEqualizerAvailable = true; - } else { - Timber.d("EqualizerController Observer.onChanged has no controller"); - isEqualizerAvailable = false; - } - } - }); - - new Thread(new Runnable() - { - @Override - public void run() - { - try - { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - jukeboxAvailable = (mediaPlayerController != null) && (mediaPlayerController.isJukeboxAvailable()); - } - catch (Exception e) - { - Timber.e(e); - } - } - }).start(); - - view.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return gestureScanner.onTouchEvent(event); - } - }); - } - - @Override - public void onResume() - { - super.onResume(); - - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController == null || mediaPlayerController.getCurrentPlaying() == null) - { - playlistFlipper.setDisplayedChild(1); - } - else - { - // Download list and Album art must be updated when Resumed - onDownloadListChanged(); - onCurrentChanged(); - } - - - final Handler handler = new Handler(); - final Runnable runnable = new Runnable() - { - @Override - public void run() - { - handler.post(new Runnable() - { - @Override - public void run() - { - update(cancellationToken); - } - }); - } - }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS); - - if (mediaPlayerController != null && mediaPlayerController.getKeepScreenOn()) - { - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - else - { - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - if (visualizerView != null) - { - visualizerView.setActive(mediaPlayerController != null && mediaPlayerController.getShowVisualization()); - } - - getActivity().invalidateOptionsMenu(); - } - - // Scroll to current playing/downloading. - private void scrollToCurrent() - { - ListAdapter adapter = playlistView.getAdapter(); - - if (adapter != null) - { - int count = adapter.getCount(); - - for (int i = 0; i < count; i++) - { - if (currentPlaying == playlistView.getItemAtPosition(i)) - { - playlistView.smoothScrollToPositionFromTop(i, 40); - return; - } - } - - final DownloadFile currentDownloading = mediaPlayerControllerLazy.getValue().getCurrentDownloading(); - for (int i = 0; i < count; i++) - { - if (currentDownloading == playlistView.getItemAtPosition(i)) - { - playlistView.smoothScrollToPositionFromTop(i, 40); - return; - } - } - } - } - - @Override - public void onPause() - { - super.onPause(); - executorService.shutdown(); - - if (visualizerView != null) - { - visualizerView.setActive(false); - } - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.nowplaying, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public void onPrepareOptionsMenu(@NotNull Menu menu) { - super.onPrepareOptionsMenu(menu); - - final MenuItem screenOption = menu.findItem(R.id.menu_item_screen_on_off); - final MenuItem jukeboxOption = menu.findItem(R.id.menu_item_jukebox); - final MenuItem equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer); - final MenuItem visualizerMenuItem = menu.findItem(R.id.menu_item_visualizer); - final MenuItem shareMenuItem = menu.findItem(R.id.menu_item_share); - starMenuItem = menu.findItem(R.id.menu_item_star); - MenuItem bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set); - MenuItem bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete); - - - if (ActiveServerProvider.Companion.isOffline()) - { - if (shareMenuItem != null) - { - shareMenuItem.setVisible(false); - } - - if (starMenuItem != null) - { - starMenuItem.setVisible(false); - } - - if (bookmarkMenuItem != null) - { - bookmarkMenuItem.setVisible(false); - } - - if (bookmarkRemoveMenuItem != null) - { - bookmarkRemoveMenuItem.setVisible(false); - } - } - - if (equalizerMenuItem != null) - { - equalizerMenuItem.setEnabled(isEqualizerAvailable); - equalizerMenuItem.setVisible(isEqualizerAvailable); - } - - if (visualizerMenuItem != null) - { - visualizerMenuItem.setEnabled(isVisualizerAvailable); - visualizerMenuItem.setVisible(isVisualizerAvailable); - } - - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController != null) - { - DownloadFile downloadFile = mediaPlayerController.getCurrentPlaying(); - - if (downloadFile != null) - { - currentSong = downloadFile.getSong(); - } - - if (useFiveStarRating) starMenuItem.setVisible(false); - - if (currentSong != null) - { - if (starMenuItem != null) - { - starMenuItem.setIcon(currentSong.getStarred() ? fullStar : hollowStar); - } - } - else - { - if (starMenuItem != null) - { - starMenuItem.setIcon(hollowStar); - } - } - - - if (mediaPlayerController.getKeepScreenOn()) - { - if (screenOption != null) - { - screenOption.setTitle(R.string.download_menu_screen_off); - } - } - else - { - if (screenOption != null) - { - screenOption.setTitle(R.string.download_menu_screen_on); - } - } - - if (jukeboxOption != null) - { - jukeboxOption.setEnabled(jukeboxAvailable); - jukeboxOption.setVisible(jukeboxAvailable); - - if (mediaPlayerController.isJukeboxEnabled()) - { - jukeboxOption.setTitle(R.string.download_menu_jukebox_off); - } - else - { - jukeboxOption.setTitle(R.string.download_menu_jukebox_on); - } - } - } - } - - @Override - public void onCreateContextMenu(final @NotNull ContextMenu menu, final @NotNull View view, final ContextMenu.ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - if (view == playlistView) - { - final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - final DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); - - final MenuInflater menuInflater = getActivity().getMenuInflater(); - menuInflater.inflate(R.menu.nowplaying_context, menu); - - MusicDirectory.Entry song = null; - - if (downloadFile != null) - { - song = downloadFile.getSong(); - } - - if (song != null && song.getParent() == null) - { - MenuItem menuItem = menu.findItem(R.id.menu_show_album); - - if (menuItem != null) - { - menuItem.setVisible(false); - } - } - - if (ActiveServerProvider.Companion.isOffline() || !Util.getShouldUseId3Tags()) - { - MenuItem menuItem = menu.findItem(R.id.menu_show_artist); - - if (menuItem != null) - { - menuItem.setVisible(false); - } - } - - if (ActiveServerProvider.Companion.isOffline()) - { - MenuItem menuItem = menu.findItem(R.id.menu_lyrics); - - if (menuItem != null) - { - menuItem.setVisible(false); - } - } - } - } - - @Override - public boolean onContextItemSelected(final MenuItem menuItem) - { - final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); - - DownloadFile downloadFile = null; - - if (info != null) - { - downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); - } - - return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return menuItemSelected(item.getItemId(), null) || super.onOptionsItemSelected(item); - } - - private boolean menuItemSelected(final int menuItemId, final DownloadFile song) - { - MusicDirectory.Entry entry = null; - Bundle bundle; - - if (song != null) - { - entry = song.getSong(); - } - - if (menuItemId == R.id.menu_show_artist) { - if (entry == null) { - return false; - } - - if (Util.getShouldUseId3Tags()) { - bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.getArtistId()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - Navigation.findNavController(getView()).navigate(R.id.playerToSelectAlbum, bundle); - } - - return true; - } else if (menuItemId == R.id.menu_show_album) { - if (entry == null) { - return false; - } - - String albumId = Util.getShouldUseId3Tags() ? entry.getAlbumId() : entry.getParent(); - bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, albumId); - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.getParent()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true); - Navigation.findNavController(getView()).navigate(R.id.playerToSelectAlbum, bundle); - return true; - } else if (menuItemId == R.id.menu_lyrics) { - if (entry == null) { - return false; - } - - bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ARTIST, entry.getArtist()); - bundle.putString(Constants.INTENT_EXTRA_NAME_TITLE, entry.getTitle()); - Navigation.findNavController(getView()).navigate(R.id.playerToLyrics, bundle); - return true; - } else if (menuItemId == R.id.menu_remove) { - mediaPlayerControllerLazy.getValue().remove(song); - onDownloadListChanged(); - return true; - } else if (menuItemId == R.id.menu_item_screen_on_off) { - if (mediaPlayerControllerLazy.getValue().getKeepScreenOn()) { - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - mediaPlayerControllerLazy.getValue().setKeepScreenOn(false); - } else { - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - mediaPlayerControllerLazy.getValue().setKeepScreenOn(true); - } - return true; - } else if (menuItemId == R.id.menu_shuffle) { - mediaPlayerControllerLazy.getValue().shuffle(); - Util.toast(getContext(), R.string.download_menu_shuffle_notification); - return true; - } else if (menuItemId == R.id.menu_item_equalizer) { - Navigation.findNavController(getView()).navigate(R.id.playerToEqualizer); - return true; - } else if (menuItemId == R.id.menu_item_visualizer) { - final boolean active = !visualizerView.isActive(); - visualizerView.setActive(active); - - if (!visualizerView.isActive()) { - visualizerViewLayout.setVisibility(View.GONE); - } else { - visualizerViewLayout.setVisibility(View.VISIBLE); - } - - mediaPlayerControllerLazy.getValue().setShowVisualization(visualizerView.isActive()); - Util.toast(getContext(), active ? R.string.download_visualizer_on : R.string.download_visualizer_off); - return true; - } else if (menuItemId == R.id.menu_item_jukebox) { - final boolean jukeboxEnabled = !mediaPlayerControllerLazy.getValue().isJukeboxEnabled(); - mediaPlayerControllerLazy.getValue().setJukeboxEnabled(jukeboxEnabled); - Util.toast(getContext(), jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false); - return true; - } else if (menuItemId == R.id.menu_item_toggle_list) { - toggleFullScreenAlbumArt(); - return true; - } else if (menuItemId == R.id.menu_item_clear_playlist) { - mediaPlayerControllerLazy.getValue().setShufflePlayEnabled(false); - mediaPlayerControllerLazy.getValue().clear(); - onDownloadListChanged(); - return true; - } else if (menuItemId == R.id.menu_item_save_playlist) { - if (mediaPlayerControllerLazy.getValue().getPlaylistSize() > 0) { - showSavePlaylistDialog(); - } - return true; - } else if (menuItemId == R.id.menu_item_star) { - if (currentSong == null) { - return true; - } - - final boolean isStarred = currentSong.getStarred(); - final String id = currentSong.getId(); - - if (isStarred) { - starMenuItem.setIcon(hollowStar); - currentSong.setStarred(false); - } else { - starMenuItem.setIcon(fullStar); - currentSong.setStarred(true); - } - - new Thread(new Runnable() { - @Override - public void run() { - final MusicService musicService = MusicServiceFactory.getMusicService(); - - try { - if (isStarred) { - musicService.unstar(id, null, null); - } else { - musicService.star(id, null, null); - } - } catch (Exception e) { - Timber.e(e); - } - } - }).start(); - - return true; - } else if (menuItemId == R.id.menu_item_bookmark_set) { - if (currentSong == null) { - return true; - } - - final String songId = currentSong.getId(); - final int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition(); - - currentSong.setBookmarkPosition(playerPosition); - - String bookmarkTime = Util.formatTotalDuration(playerPosition, true); - - new Thread(new Runnable() { - @Override - public void run() { - final MusicService musicService = MusicServiceFactory.getMusicService(); - - try { - musicService.createBookmark(songId, playerPosition); - } catch (Exception e) { - Timber.e(e); - } - } - }).start(); - - String msg = getResources().getString(R.string.download_bookmark_set_at_position, bookmarkTime); - - Util.toast(getContext(), msg); - - return true; - } else if (menuItemId == R.id.menu_item_bookmark_delete) { - if (currentSong == null) { - return true; - } - - final String bookmarkSongId = currentSong.getId(); - currentSong.setBookmarkPosition(0); - - new Thread(new Runnable() { - @Override - public void run() { - final MusicService musicService = MusicServiceFactory.getMusicService(); - - try { - musicService.deleteBookmark(bookmarkSongId); - } catch (Exception e) { - Timber.e(e); - } - } - }).start(); - - Util.toast(getContext(), R.string.download_bookmark_removed); - - return true; - } else if (menuItemId == R.id.menu_item_share) { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - List entries = new ArrayList<>(); - - if (mediaPlayerController != null) { - List downloadServiceSongs = mediaPlayerController.getPlayList(); - - if (downloadServiceSongs != null) { - for (DownloadFile downloadFile : downloadServiceSongs) { - if (downloadFile != null) { - MusicDirectory.Entry playlistEntry = downloadFile.getSong(); - - if (playlistEntry != null) { - entries.add(playlistEntry); - } - } - } - } - } - - shareHandler.getValue().createShare(this, entries, null, cancellationToken); - return true; - } - return false; - } - - private void update(CancellationToken cancel) - { - if (cancel.isCancellationRequested()) return; - - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if (mediaPlayerController == null) - { - return; - } - - if (currentRevision != mediaPlayerController.getPlayListUpdateRevision()) - { - onDownloadListChanged(); - } - - if (currentPlaying != mediaPlayerController.getCurrentPlaying()) - { - onCurrentChanged(); - } - - onSliderProgressChanged(); - getActivity().invalidateOptionsMenu(); - } - - private void savePlaylistInBackground(final String playlistName) - { - Util.toast(getContext(), getResources().getString(R.string.download_playlist_saving, playlistName)); - mediaPlayerControllerLazy.getValue().setSuggestedPlaylistName(playlistName); - new SilentBackgroundTask(getActivity()) - { - @Override - protected Void doInBackground() throws Throwable - { - final List entries = new LinkedList<>(); - for (final DownloadFile downloadFile : mediaPlayerControllerLazy.getValue().getPlayList()) - { - entries.add(downloadFile.getSong()); - } - final MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.createPlaylist(null, playlistName, entries); - return null; - } - - @Override - protected void done(final Void result) - { - Util.toast(getContext(), R.string.download_playlist_done); - } - - @Override - protected void error(final Throwable error) - { - Timber.e(error, "Exception has occurred in savePlaylistInBackground"); - final String msg = String.format("%s %s", getResources().getString(R.string.download_playlist_error), getErrorMessage(error)); - Util.toast(getContext(), msg); - } - }.execute(); - } - - private void toggleFullScreenAlbumArt() - { - if (playlistFlipper.getDisplayedChild() == 1) - { - playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.push_down_in)); - playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.push_down_out)); - playlistFlipper.setDisplayedChild(0); - } - else - { - playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.push_up_in)); - playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.push_up_out)); - playlistFlipper.setDisplayedChild(1); - } - - scrollToCurrent(); - } - - private void start() - { - final MediaPlayerController service = mediaPlayerControllerLazy.getValue(); - final PlayerState state = service.getPlayerState(); - - if (state == PAUSED || state == COMPLETED || state == STOPPED) - { - service.start(); - } - else if (state == IDLE) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - - final int current = mediaPlayerControllerLazy.getValue().getCurrentPlayingNumberOnPlaylist(); - - if (current == -1) - { - service.play(0); - } - else - { - service.play(current); - } - } - } - - private void onDownloadListChanged() - { - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if (mediaPlayerController == null) - { - return; - } - - final List list = mediaPlayerController.getPlayList(); - - emptyTextView.setText(R.string.download_empty); - final SongListAdapter adapter = new SongListAdapter(getContext(), list); - playlistView.setAdapter(adapter); - - playlistView.setDragSortListener(new DragSortListView.DragSortListener() - { - @Override - public void drop(int from, int to) - { - if (from != to) - { - DownloadFile item = adapter.getItem(from); - adapter.remove(item); - adapter.notifyDataSetChanged(); - adapter.insert(item, to); - adapter.notifyDataSetChanged(); - } - } - - @Override - public void drag(int from, int to) - { - - } - - @Override - public void remove(int which) - { - DownloadFile item = adapter.getItem(which); - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (item == null || mediaPlayerController == null) - { - return; - } - - DownloadFile currentPlaying = mediaPlayerController.getCurrentPlaying(); - - if (currentPlaying == item) - { - mediaPlayerControllerLazy.getValue().next(); - } - - adapter.remove(item); - adapter.notifyDataSetChanged(); - - String songRemoved = String.format(getResources().getString(R.string.download_song_removed), item.getSong().getTitle()); - - Util.toast(getContext(), songRemoved); - - onDownloadListChanged(); - onCurrentChanged(); - } - }); - - emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); - currentRevision = mediaPlayerController.getPlayListUpdateRevision(); - - switch (mediaPlayerController.getRepeatMode()) - { - case OFF: - repeatButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_repeat_off)); - break; - case ALL: - repeatButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_repeat_all)); - break; - case SINGLE: - repeatButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_repeat_single)); - break; - default: - break; - } - } - - private void onCurrentChanged() - { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController == null) - { - return; - } - - currentPlaying = mediaPlayerController.getCurrentPlaying(); - - scrollToCurrent(); - - long totalDuration = mediaPlayerController.getPlayListDuration(); - long totalSongs = mediaPlayerController.getPlaylistSize(); - int currentSongIndex = mediaPlayerController.getCurrentPlayingNumberOnPlaylist() + 1; - - String duration = Util.formatTotalDuration(totalDuration); - - String trackFormat = String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs); - - if (currentPlaying != null) - { - currentSong = currentPlaying.getSong(); - songTitleTextView.setText(currentSong.getTitle()); - albumTextView.setText(currentSong.getAlbum()); - artistTextView.setText(currentSong.getArtist()); - downloadTrackTextView.setText(trackFormat); - downloadTotalDurationTextView.setText(duration); - imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0); - - displaySongRating(); - } - else - { - currentSong = null; - songTitleTextView.setText(null); - albumTextView.setText(null); - artistTextView.setText(null); - downloadTrackTextView.setText(null); - downloadTotalDurationTextView.setText(null); - imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0); - } - } - - private void onSliderProgressChanged() - { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController == null || onProgressChangedTask != null) - { - return; - } - - onProgressChangedTask = new SilentBackgroundTask(getActivity()) - { - MediaPlayerController mediaPlayerController; - boolean isJukeboxEnabled; - int millisPlayed; - Integer duration; - PlayerState playerState; - - @Override - protected Void doInBackground() - { - this.mediaPlayerController = mediaPlayerControllerLazy.getValue(); - isJukeboxEnabled = this.mediaPlayerController.isJukeboxEnabled(); - millisPlayed = Math.max(0, this.mediaPlayerController.getPlayerPosition()); - duration = this.mediaPlayerController.getPlayerDuration(); - playerState = mediaPlayerControllerLazy.getValue().getPlayerState(); - return null; - } - - @Override - protected void done(final Void result) - { - if (cancellationToken.isCancellationRequested()) return; - if (currentPlaying != null) - { - final int millisTotal = duration == null ? 0 : duration; - - positionTextView.setText(Util.formatTotalDuration(millisPlayed, true)); - durationTextView.setText(Util.formatTotalDuration(millisTotal, true)); - progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug. - progressBar.setProgress(millisPlayed); - progressBar.setEnabled(currentPlaying.isWorkDone() || isJukeboxEnabled); - } - else - { - positionTextView.setText(R.string.util_zero_time); - durationTextView.setText(R.string.util_no_time); - progressBar.setProgress(0); - progressBar.setMax(0); - progressBar.setEnabled(false); - } - - switch (playerState) - { - case DOWNLOADING: - int progress = currentPlaying != null ? currentPlaying.getProgress().getValue() : 0; - String downloadStatus = getResources().getString(R.string.download_playerstate_downloading, Util.formatPercentage(progress)); - FragmentTitle.Companion.setTitle(PlayerFragment.this, downloadStatus); - break; - case PREPARING: - FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_buffering); - break; - case STARTED: - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController != null && mediaPlayerController.isShufflePlayEnabled()) - { - FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_playing_shuffle); - } - else - { - FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname); - } - break; - default: - FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname); - break; - case IDLE: - case PREPARED: - case STOPPED: - case PAUSED: - case COMPLETED: - break; - } - - switch (playerState) - { - case STARTED: - pauseButton.setVisibility(View.VISIBLE); - stopButton.setVisibility(View.GONE); - startButton.setVisibility(View.GONE); - break; - case DOWNLOADING: - case PREPARING: - pauseButton.setVisibility(View.GONE); - stopButton.setVisibility(View.VISIBLE); - startButton.setVisibility(View.GONE); - break; - default: - pauseButton.setVisibility(View.GONE); - stopButton.setVisibility(View.GONE); - startButton.setVisibility(View.VISIBLE); - break; - } - - // TODO: It would be a lot nicer if MediaPlayerController would send an event when this is necessary instead of updating every time - displaySongRating(); - - onProgressChangedTask = null; - } - }; - onProgressChangedTask.execute(); - } - - private void changeProgress(final int ms) - { - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if (mediaPlayerController == null) - { - return; - } - - new SilentBackgroundTask(getActivity()) - { - int msPlayed; - Integer duration; - int seekTo; - - @Override - protected Void doInBackground() - { - msPlayed = Math.max(0, mediaPlayerController.getPlayerPosition()); - duration = mediaPlayerController.getPlayerDuration(); - - final int msTotal = duration; - seekTo = Math.min(msPlayed + ms, msTotal); - mediaPlayerController.seekTo(seekTo); - return null; - } - - @Override - protected void done(final Void result) - { - progressBar.setProgress(seekTo); - } - }.execute(); - } - - @Override - public boolean onDown(final MotionEvent me) - { - return false; - } - - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) - { - - final MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - - if (mediaPlayerController == null || e1 == null || e2 == null) - { - return false; - } - - float e1X = e1.getX(); - float e2X = e2.getX(); - float e1Y = e1.getY(); - float e2Y = e2.getY(); - float absX = Math.abs(velocityX); - float absY = Math.abs(velocityY); - - // Right to Left swipe - if (e1X - e2X > swipeDistance && absX > swipeVelocity) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.next(); - onCurrentChanged(); - onSliderProgressChanged(); - return true; - } - - // Left to Right swipe - if (e2X - e1X > swipeDistance && absX > swipeVelocity) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.previous(); - onCurrentChanged(); - onSliderProgressChanged(); - return true; - } - - // Top to Bottom swipe - if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() + 30000); - onSliderProgressChanged(); - return true; - } - - // Bottom to Top swipe - if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() - 8000); - onSliderProgressChanged(); - return true; - } - - return false; - } - - @Override - public void onLongPress(final MotionEvent e) - { - } - - @Override - public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, final float distanceY) - { - return false; - } - - @Override - public void onShowPress(final MotionEvent e) - { - } - - @Override - public boolean onSingleTapUp(final MotionEvent e) - { - return false; - } - - public static SeekBar getProgressBar() - { - return progressBar; - } - - private void displaySongRating() - { - int rating = currentSong == null || currentSong.getUserRating() == null ? 0 : currentSong.getUserRating(); - fiveStar1ImageView.setImageDrawable(rating > 0 ? fullStar : hollowStar); - fiveStar2ImageView.setImageDrawable(rating > 1 ? fullStar : hollowStar); - fiveStar3ImageView.setImageDrawable(rating > 2 ? fullStar : hollowStar); - fiveStar4ImageView.setImageDrawable(rating > 3 ? fullStar : hollowStar); - fiveStar5ImageView.setImageDrawable(rating > 4 ? fullStar : hollowStar); - } - - private void setSongRating(final int rating) - { - if (currentSong == null) - return; - - displaySongRating(); - mediaPlayerControllerLazy.getValue().setSongRating(rating); - } - - private void showSavePlaylistDialog() { - final AlertDialog.Builder builder; - - final LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE); - final View layout = layoutInflater.inflate(R.layout.save_playlist, (ViewGroup) getActivity().findViewById(R.id.save_playlist_root)); - - if (layout != null) - { - playlistNameView = layout.findViewById(R.id.save_playlist_name); - } - - builder = new AlertDialog.Builder(getContext()); - builder.setTitle(R.string.download_playlist_title); - builder.setMessage(R.string.download_playlist_name); - builder.setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() - { - @Override - public void onClick(final DialogInterface dialog, final int clickId) - { - savePlaylistInBackground(String.valueOf(playlistNameView.getText())); - } - }); - builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() - { - @Override - public void onClick(final DialogInterface dialog, final int clickId) - { - dialog.cancel(); - } - }); - builder.setView(layout); - builder.setCancelable(true); - - AlertDialog dialog = builder.create(); - - final String playlistName = mediaPlayerControllerLazy.getValue().getSuggestedPlaylistName(); - if (playlistName != null) - { - playlistNameView.setText(playlistName); - } - else - { - final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - playlistNameView.setText(dateFormat.format(new Date())); - } - - dialog.show(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java index b47929d9..f768de46 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java @@ -1,7 +1,5 @@ package org.moire.ultrasonic.service; -import timber.log.Timber; - import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.util.LRUCache; import org.moire.ultrasonic.util.ShufflePlayBuffer; @@ -16,6 +14,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import kotlin.Lazy; +import timber.log.Timber; import static org.koin.java.KoinJavaComponent.inject; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; @@ -342,7 +341,7 @@ public class Downloader Collections.shuffle(downloadList); if (localMediaPlayer.currentPlaying != null) { - downloadList.remove(getCurrentPlayingIndex()); + downloadList.remove(localMediaPlayer.currentPlaying); downloadList.add(0, localMediaPlayer.currentPlaying); } revision++; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SilentBackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/SilentBackgroundTask.java deleted file mode 100644 index 6e751ed1..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SilentBackgroundTask.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import android.app.Activity; - -/** - * @author Sindre Mehus - */ -public abstract class SilentBackgroundTask extends BackgroundTask -{ - - public SilentBackgroundTask(Activity activity) - { - super(activity); - } - - @Override - public void execute() - { - Thread thread = new Thread() - { - @Override - public void run() - { - try - { - final T result = doInBackground(); - - getHandler().post(new Runnable() - { - @Override - public void run() - { - done(result); - } - }); - - } - catch (final Throwable t) - { - getHandler().post(new Runnable() - { - @Override - public void run() - { - error(t); - } - }); - } - } - }; - thread.start(); - } - - @Override - public void updateProgress(int messageId) - { - } - - @Override - public void updateProgress(String message) - { - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 75b93057..97285d68 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -49,8 +49,9 @@ class AlbumListFragment : GenericListFragment> = MutableLiveData() + val albumList: MutableLiveData> = MutableLiveData(listOf()) + var lastType: String? = null private var loadedUntil: Int = 0 fun getAlbumList( @@ -21,8 +22,14 @@ class AlbumListModel(application: Application) : GenericListModel(application) { swipe: SwipeRefreshLayout?, args: Bundle ): LiveData> { + // Don't reload the data if navigating back to the view that was active before. + // This way, we keep the scroll position + val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! - backgroundLoadFromServer(refresh, swipe, args) + if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) { + lastType = albumListType + backgroundLoadFromServer(refresh, swipe, args) + } return albumList } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index 63e9e907..02abc9f5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -30,13 +30,17 @@ import org.moire.ultrasonic.service.MusicService * Provides ViewModel which contains the list of available Artists */ class ArtistListModel(application: Application) : GenericListModel(application) { - val artists: MutableLiveData> = MutableLiveData() + val artists: MutableLiveData> = MutableLiveData(listOf()) /** * Retrieves all available Artists in a LiveData */ fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData> { - backgroundLoadFromServer(refresh, swipe) + // Don't reload the data if navigating back to the view that was active before. + // This way, we keep the scroll position + if (artists.value!!.isEmpty() || refresh) { + backgroundLoadFromServer(refresh, swipe) + } return artists } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt index 2a26463c..b825fcac 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt @@ -16,20 +16,24 @@ import android.widget.ImageView import android.widget.PopupMenu import android.widget.RelativeLayout import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.view.SelectMusicFolderView /* * An abstract Adapter, which can be extended to display a List of in a RecyclerView */ -abstract class GenericRowAdapter( +abstract class GenericRowAdapter( val onItemClick: (T) -> Unit, val onContextMenuClick: (MenuItem, T) -> Boolean, private val onMusicFolderUpdate: (String?) -> Unit -) : RecyclerView.Adapter() { +) : ListAdapter(GenericDiffCallback()) { + open var itemList: List = listOf() protected abstract val layout: Int protected abstract val contextMenuLayout: Int @@ -40,11 +44,12 @@ abstract class GenericRowAdapter( var selectedFolder: String? = null /** - * Sets the data to be displayed in the RecyclerView + * Sets the data to be displayed in the RecyclerView, + * using DiffUtil to efficiently calculate the minimum required changes.. */ open fun setData(data: List) { + submitList(data) itemList = data - notifyDataSetChanged() } /** @@ -136,5 +141,17 @@ abstract class GenericRowAdapter( companion object { internal const val TYPE_HEADER = 0 internal const val TYPE_ITEM = 1 + + /** + * Calculates the differences between data sets + */ + class GenericDiffCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem == newItem + } + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem.id == newItem.id + } + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt new file mode 100644 index 00000000..af4f7be9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -0,0 +1,1203 @@ +/* + * PlayerFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.graphics.Point +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Handler +import android.view.ContextMenu +import android.view.ContextMenu.ContextMenuInfo +import android.view.GestureDetector +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.animation.AnimationUtils +import android.widget.AdapterView.AdapterContextMenuInfo +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import android.widget.TextView +import android.widget.ViewFlipper +import androidx.fragment.app.Fragment +import androidx.navigation.Navigation +import com.mobeta.android.dslv.DragSortListView +import com.mobeta.android.dslv.DragSortListView.DragSortListener +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.ArrayList +import java.util.Date +import java.util.LinkedList +import java.util.Locale +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import kotlin.math.abs +import kotlin.math.max +import org.koin.android.ext.android.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.moire.ultrasonic.R +import org.moire.ultrasonic.audiofx.EqualizerController +import org.moire.ultrasonic.audiofx.VisualizerController +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.domain.RepeatMode +import org.moire.ultrasonic.featureflags.Feature +import org.moire.ultrasonic.featureflags.FeatureStorage +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.service.DownloadFile +import org.moire.ultrasonic.service.LocalMediaPlayer +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.subsonic.ImageLoaderProvider +import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker +import org.moire.ultrasonic.subsonic.ShareHandler +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.SilentBackgroundTask +import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.view.AutoRepeatButton +import org.moire.ultrasonic.view.SongListAdapter +import org.moire.ultrasonic.view.VisualizerView +import timber.log.Timber + +/** + * Contains the Music Player screen of Ultrasonic with playback controls and the playlist + * + * TODO: This class was more or less straight converted from Java legacy code. + * There are many places where further cleanup would be nice. + * The usage of threads and SilentBackgroundTask can be replaced with Coroutines. + */ +@Suppress("LargeClass", "TooManyFunctions", "MagicNumber") +class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent { + // Settings + private var currentRevision: Long = 0 + private var swipeDistance = 0 + private var swipeVelocity = 0 + private var jukeboxAvailable = false + private var useFiveStarRating = false + private var isEqualizerAvailable = false + private var isVisualizerAvailable = false + + // Detectors & Callbacks + private lateinit var gestureScanner: GestureDetector + private lateinit var cancellationToken: CancellationToken + + // Data & Services + private val networkAndStorageChecker: NetworkAndStorageChecker by inject() + private val mediaPlayerController: MediaPlayerController by inject() + private val localMediaPlayer: LocalMediaPlayer by inject() + private val shareHandler: ShareHandler by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() + private lateinit var executorService: ScheduledExecutorService + private var currentPlaying: DownloadFile? = null + private var currentSong: MusicDirectory.Entry? = null + private var onProgressChangedTask: SilentBackgroundTask? = null + + // Views and UI Elements + private lateinit var visualizerViewLayout: LinearLayout + private lateinit var visualizerView: VisualizerView + private lateinit var playlistNameView: EditText + private lateinit var starMenuItem: MenuItem + private lateinit var fiveStar1ImageView: ImageView + private lateinit var fiveStar2ImageView: ImageView + private lateinit var fiveStar3ImageView: ImageView + private lateinit var fiveStar4ImageView: ImageView + private lateinit var fiveStar5ImageView: ImageView + private lateinit var playlistFlipper: ViewFlipper + private lateinit var emptyTextView: TextView + private lateinit var songTitleTextView: TextView + private lateinit var albumTextView: TextView + private lateinit var artistTextView: TextView + private lateinit var albumArtImageView: ImageView + private lateinit var playlistView: DragSortListView + private lateinit var positionTextView: TextView + private lateinit var downloadTrackTextView: TextView + private lateinit var downloadTotalDurationTextView: TextView + private lateinit var durationTextView: TextView + private lateinit var pauseButton: View + private lateinit var stopButton: View + private lateinit var startButton: View + private lateinit var repeatButton: ImageView + private lateinit var hollowStar: Drawable + private lateinit var fullStar: Drawable + private lateinit var progressBar: SeekBar + + override fun onCreate(savedInstanceState: Bundle?) { + Util.applyTheme(this.context) + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.current_playing, container, false) + } + + fun findViews(view: View) { + playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper) + emptyTextView = view.findViewById(R.id.playlist_empty) + songTitleTextView = view.findViewById(R.id.current_playing_song) + albumTextView = view.findViewById(R.id.current_playing_album) + artistTextView = view.findViewById(R.id.current_playing_artist) + albumArtImageView = view.findViewById(R.id.current_playing_album_art_image) + positionTextView = view.findViewById(R.id.current_playing_position) + downloadTrackTextView = view.findViewById(R.id.current_playing_track) + downloadTotalDurationTextView = view.findViewById(R.id.current_total_duration) + durationTextView = view.findViewById(R.id.current_playing_duration) + progressBar = view.findViewById(R.id.current_playing_progress_bar) + playlistView = view.findViewById(R.id.playlist_view) + + pauseButton = view.findViewById(R.id.button_pause) + stopButton = view.findViewById(R.id.button_stop) + startButton = view.findViewById(R.id.button_start) + repeatButton = view.findViewById(R.id.button_repeat) + visualizerViewLayout = view.findViewById(R.id.current_playing_visualizer_layout) + fiveStar1ImageView = view.findViewById(R.id.song_five_star_1) + fiveStar2ImageView = view.findViewById(R.id.song_five_star_2) + fiveStar3ImageView = view.findViewById(R.id.song_five_star_3) + fiveStar4ImageView = view.findViewById(R.id.song_five_star_4) + fiveStar5ImageView = view.findViewById(R.id.song_five_star_5) + } + + @Suppress("LongMethod") + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cancellationToken = CancellationToken() + setTitle(this, R.string.common_appname) + val windowManager = requireActivity().windowManager + val display = windowManager.defaultDisplay + val size = Point() + display.getSize(size) + val width = size.x + val height = size.y + setHasOptionsMenu(true) + useFiveStarRating = get().isFeatureEnabled(Feature.FIVE_STAR_RATING) + swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100 + swipeVelocity = swipeDistance + gestureScanner = GestureDetector(context, this) + + // The secondary progress is an indicator of how far the song is cached. + localMediaPlayer.secondaryProgress.observe( + viewLifecycleOwner, + { + progressBar.secondaryProgress = it + } + ) + + findViews(view) + val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) + val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) + val shuffleButton = view.findViewById(R.id.button_shuffle) + val ratingLinearLayout = view.findViewById(R.id.song_rating) + if (!useFiveStarRating) ratingLinearLayout.visibility = View.GONE + hollowStar = Util.getDrawableFromAttribute(view.context, R.attr.star_hollow) + fullStar = Util.getDrawableFromAttribute(context, R.attr.star_full) + + fiveStar1ImageView.setOnClickListener { setSongRating(1) } + fiveStar2ImageView.setOnClickListener { setSongRating(2) } + fiveStar3ImageView.setOnClickListener { setSongRating(3) } + fiveStar4ImageView.setOnClickListener { setSongRating(4) } + fiveStar5ImageView.setOnClickListener { setSongRating(5) } + + albumArtImageView.setOnTouchListener { _, me -> + gestureScanner.onTouchEvent(me) + } + + albumArtImageView.setOnClickListener { + toggleFullScreenAlbumArt() + } + + previousButton.setOnClickListener { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + mediaPlayerController.previous() + return null + } + + override fun done(result: Void?) { + onCurrentChanged() + onSliderProgressChanged() + } + }.execute() + } + + previousButton.setOnRepeatListener { + val incrementTime = Util.getIncrementTime() + changeProgress(-incrementTime) + } + + nextButton.setOnClickListener { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Boolean { + mediaPlayerController.next() + return true + } + + override fun done(result: Boolean?) { + if (result == true) { + onCurrentChanged() + onSliderProgressChanged() + } + } + }.execute() + } + + nextButton.setOnRepeatListener { + val incrementTime = Util.getIncrementTime() + changeProgress(incrementTime) + } + pauseButton.setOnClickListener { + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + mediaPlayerController.pause() + return null + } + + override fun done(result: Void?) { + onCurrentChanged() + onSliderProgressChanged() + } + }.execute() + } + stopButton.setOnClickListener { + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + mediaPlayerController.reset() + return null + } + + override fun done(result: Void?) { + onCurrentChanged() + onSliderProgressChanged() + } + }.execute() + } + startButton.setOnClickListener { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + start() + return null + } + + override fun done(result: Void?) { + onCurrentChanged() + onSliderProgressChanged() + } + }.execute() + } + shuffleButton.setOnClickListener { + mediaPlayerController.shuffle() + Util.toast(activity, R.string.download_menu_shuffle_notification) + } + + repeatButton.setOnClickListener { + val repeatMode = mediaPlayerController.repeatMode?.next() + mediaPlayerController.repeatMode = repeatMode + onDownloadListChanged() + when (repeatMode) { + RepeatMode.OFF -> Util.toast( + context, R.string.download_repeat_off + ) + RepeatMode.ALL -> Util.toast( + context, R.string.download_repeat_all + ) + RepeatMode.SINGLE -> Util.toast( + context, R.string.download_repeat_single + ) + else -> { + } + } + } + + progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onStopTrackingTouch(seekBar: SeekBar) { + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + mediaPlayerController.seekTo(progressBar.progress) + return null + } + + override fun done(result: Void?) { + onSliderProgressChanged() + } + }.execute() + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + }) + + playlistView.setOnItemClickListener { _, _, position, _ -> + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + object : SilentBackgroundTask(activity) { + override fun doInBackground(): Void? { + mediaPlayerController.play(position) + return null + } + + override fun done(result: Void?) { + onCurrentChanged() + onSliderProgressChanged() + } + }.execute() + } + registerForContextMenu(playlistView) + + if (arguments != null && requireArguments().getBoolean( + Constants.INTENT_EXTRA_NAME_SHUFFLE, + false + ) + ) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.isShufflePlayEnabled = true + } + + visualizerViewLayout.visibility = View.GONE + VisualizerController.get().observe( + requireActivity(), + { visualizerController -> + if (visualizerController != null) { + Timber.d("VisualizerController Observer.onChanged received controller") + visualizerView = VisualizerView(context) + visualizerViewLayout.addView( + visualizerView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + ) + if (!visualizerView.isActive) { + visualizerViewLayout.visibility = View.GONE + } else { + visualizerViewLayout.visibility = View.VISIBLE + } + visualizerView.setOnTouchListener { _, _ -> + visualizerView.isActive = !visualizerView.isActive + mediaPlayerController.showVisualization = visualizerView.isActive + true + } + isVisualizerAvailable = true + } else { + Timber.d("VisualizerController Observer.onChanged has no controller") + visualizerViewLayout.visibility = View.GONE + isVisualizerAvailable = false + } + } + ) + + EqualizerController.get().observe( + requireActivity(), + { equalizerController -> + isEqualizerAvailable = if (equalizerController != null) { + Timber.d("EqualizerController Observer.onChanged received controller") + true + } else { + Timber.d("EqualizerController Observer.onChanged has no controller") + false + } + } + ) + Thread { + try { + jukeboxAvailable = mediaPlayerController.isJukeboxAvailable + } catch (all: Exception) { + Timber.e(all) + } + }.start() + view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) } + } + + override fun onResume() { + super.onResume() + if (mediaPlayerController.currentPlaying == null) { + playlistFlipper.displayedChild = 1 + } else { + // Download list and Album art must be updated when Resumed + onDownloadListChanged() + onCurrentChanged() + } + val handler = Handler() + val runnable = Runnable { handler.post { update(cancellationToken) } } + executorService = Executors.newSingleThreadScheduledExecutor() + executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS) + + if (mediaPlayerController.keepScreenOn) { + requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + if (::visualizerView.isInitialized) { + visualizerView.isActive = mediaPlayerController.showVisualization + } + + requireActivity().invalidateOptionsMenu() + } + + // Scroll to current playing/downloading. + private fun scrollToCurrent() { + val adapter = playlistView.adapter + if (adapter != null) { + val count = adapter.count + for (i in 0 until count) { + if (currentPlaying == playlistView.getItemAtPosition(i)) { + playlistView.smoothScrollToPositionFromTop(i, 40) + return + } + } + val currentDownloading = mediaPlayerController.currentDownloading + for (i in 0 until count) { + if (currentDownloading == playlistView.getItemAtPosition(i)) { + playlistView.smoothScrollToPositionFromTop(i, 40) + return + } + } + } + } + + override fun onPause() { + super.onPause() + executorService.shutdown() + if (::visualizerView.isInitialized) { + visualizerView.isActive = mediaPlayerController.showVisualization + } + } + + override fun onDestroyView() { + cancellationToken.cancel() + super.onDestroyView() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.nowplaying, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + val screenOption = menu.findItem(R.id.menu_item_screen_on_off) + val jukeboxOption = menu.findItem(R.id.menu_item_jukebox) + val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer) + val visualizerMenuItem = menu.findItem(R.id.menu_item_visualizer) + val shareMenuItem = menu.findItem(R.id.menu_item_share) + starMenuItem = menu.findItem(R.id.menu_item_star) + val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set) + val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete) + + if (isOffline()) { + if (shareMenuItem != null) { + shareMenuItem.isVisible = false + } + starMenuItem.isVisible = false + if (bookmarkMenuItem != null) { + bookmarkMenuItem.isVisible = false + } + if (bookmarkRemoveMenuItem != null) { + bookmarkRemoveMenuItem.isVisible = false + } + } + if (equalizerMenuItem != null) { + equalizerMenuItem.isEnabled = isEqualizerAvailable + equalizerMenuItem.isVisible = isEqualizerAvailable + } + if (visualizerMenuItem != null) { + visualizerMenuItem.isEnabled = isVisualizerAvailable + visualizerMenuItem.isVisible = isVisualizerAvailable + } + val mediaPlayerController = mediaPlayerController + val downloadFile = mediaPlayerController.currentPlaying + if (downloadFile != null) { + currentSong = downloadFile.song + } + if (useFiveStarRating) starMenuItem.isVisible = false + if (currentSong != null) { + starMenuItem.icon = if (currentSong!!.starred) fullStar else hollowStar + } else { + starMenuItem.icon = hollowStar + } + if (mediaPlayerController.keepScreenOn) { + screenOption?.setTitle(R.string.download_menu_screen_off) + } else { + screenOption?.setTitle(R.string.download_menu_screen_on) + } + if (jukeboxOption != null) { + jukeboxOption.isEnabled = jukeboxAvailable + jukeboxOption.isVisible = jukeboxAvailable + if (mediaPlayerController.isJukeboxEnabled) { + jukeboxOption.setTitle(R.string.download_menu_jukebox_off) + } else { + jukeboxOption.setTitle(R.string.download_menu_jukebox_on) + } + } + } + + override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { + super.onCreateContextMenu(menu, view, menuInfo) + if (view === playlistView) { + val info = menuInfo as AdapterContextMenuInfo? + val downloadFile = playlistView.getItemAtPosition(info!!.position) as DownloadFile + val menuInflater = requireActivity().menuInflater + menuInflater.inflate(R.menu.nowplaying_context, menu) + val song: MusicDirectory.Entry? + + song = downloadFile.song + + if (song.parent == null) { + val menuItem = menu.findItem(R.id.menu_show_album) + if (menuItem != null) { + menuItem.isVisible = false + } + } + + if (isOffline() || !Util.getShouldUseId3Tags()) { + menu.findItem(R.id.menu_show_artist)?.isVisible = false + } + + if (isOffline()) { + menu.findItem(R.id.menu_lyrics)?.isVisible = false + } + } + } + + override fun onContextItemSelected(menuItem: MenuItem): Boolean { + val info = menuItem.menuInfo as AdapterContextMenuInfo + val downloadFile = playlistView.getItemAtPosition(info.position) as DownloadFile + return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected( + menuItem + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return menuItemSelected(item.itemId, null) || super.onOptionsItemSelected(item) + } + + @Suppress("ComplexMethod", "LongMethod", "ReturnCount") + private fun menuItemSelected(menuItemId: Int, song: DownloadFile?): Boolean { + var entry: MusicDirectory.Entry? = null + val bundle: Bundle + if (song != null) { + entry = song.song + } + + when (menuItemId) { + R.id.menu_show_artist -> { + if (entry == null) { + return false + } + if (Util.getShouldUseId3Tags()) { + bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.artistId) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.artist) + bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.artistId) + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true) + Navigation.findNavController(requireView()) + .navigate(R.id.playerToSelectAlbum, bundle) + } + return true + } + R.id.menu_show_album -> { + if (entry == null) { + return false + } + val albumId = if (Util.getShouldUseId3Tags()) entry.albumId else entry.parent + bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, albumId) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.album) + bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent) + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true) + Navigation.findNavController(requireView()) + .navigate(R.id.playerToSelectAlbum, bundle) + return true + } + R.id.menu_lyrics -> { + if (entry == null) { + return false + } + bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ARTIST, entry.artist) + bundle.putString(Constants.INTENT_EXTRA_NAME_TITLE, entry.title) + Navigation.findNavController(requireView()).navigate(R.id.playerToLyrics, bundle) + return true + } + R.id.menu_remove -> { + mediaPlayerController.remove(song!!) + onDownloadListChanged() + return true + } + R.id.menu_item_screen_on_off -> { + val window = requireActivity().window + if (mediaPlayerController.keepScreenOn) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + mediaPlayerController.keepScreenOn = false + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + mediaPlayerController.keepScreenOn = true + } + return true + } + R.id.menu_shuffle -> { + mediaPlayerController.shuffle() + Util.toast(context, R.string.download_menu_shuffle_notification) + return true + } + R.id.menu_item_equalizer -> { + Navigation.findNavController(requireView()).navigate(R.id.playerToEqualizer) + return true + } + R.id.menu_item_visualizer -> { + val active = !visualizerView.isActive + visualizerView.isActive = active + if (!visualizerView.isActive) { + visualizerViewLayout.visibility = View.GONE + } else { + visualizerViewLayout.visibility = View.VISIBLE + } + mediaPlayerController.showVisualization = visualizerView.isActive + Util.toast( + context, + if (active) R.string.download_visualizer_on + else R.string.download_visualizer_off + ) + return true + } + R.id.menu_item_jukebox -> { + val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled + mediaPlayerController.isJukeboxEnabled = jukeboxEnabled + Util.toast( + context, + if (jukeboxEnabled) R.string.download_jukebox_on + else R.string.download_jukebox_off, + false + ) + return true + } + R.id.menu_item_toggle_list -> { + toggleFullScreenAlbumArt() + return true + } + R.id.menu_item_clear_playlist -> { + mediaPlayerController.isShufflePlayEnabled = false + mediaPlayerController.clear() + onDownloadListChanged() + return true + } + R.id.menu_item_save_playlist -> { + if (mediaPlayerController.playlistSize > 0) { + showSavePlaylistDialog() + } + return true + } + R.id.menu_item_star -> { + if (currentSong == null) { + return true + } + val isStarred = currentSong!!.starred + val id = currentSong!!.id + if (isStarred) { + starMenuItem.icon = hollowStar + currentSong!!.starred = false + } else { + starMenuItem.icon = fullStar + currentSong!!.starred = true + } + Thread { + val musicService = getMusicService() + try { + if (isStarred) { + musicService.unstar(id, null, null) + } else { + musicService.star(id, null, null) + } + } catch (all: Exception) { + Timber.e(all) + } + }.start() + return true + } + R.id.menu_item_bookmark_set -> { + if (currentSong == null) { + return true + } + val songId = currentSong!!.id + val playerPosition = mediaPlayerController.playerPosition + currentSong!!.bookmarkPosition = playerPosition + val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true) + Thread { + val musicService = getMusicService() + try { + musicService.createBookmark(songId, playerPosition) + } catch (all: Exception) { + Timber.e(all) + } + }.start() + val msg = resources.getString( + R.string.download_bookmark_set_at_position, + bookmarkTime + ) + Util.toast(context, msg) + return true + } + R.id.menu_item_bookmark_delete -> { + if (currentSong == null) { + return true + } + val bookmarkSongId = currentSong!!.id + currentSong!!.bookmarkPosition = 0 + Thread { + val musicService = getMusicService() + try { + musicService.deleteBookmark(bookmarkSongId) + } catch (all: Exception) { + Timber.e(all) + } + }.start() + Util.toast(context, R.string.download_bookmark_removed) + return true + } + R.id.menu_item_share -> { + val mediaPlayerController = mediaPlayerController + val entries: MutableList = ArrayList() + val downloadServiceSongs = mediaPlayerController.playList + for (downloadFile in downloadServiceSongs) { + val playlistEntry = downloadFile.song + entries.add(playlistEntry) + } + shareHandler.createShare(this, entries, null, cancellationToken) + return true + } + else -> return false + } + } + + private fun update(cancel: CancellationToken?) { + if (cancel!!.isCancellationRequested) return + val mediaPlayerController = mediaPlayerController + if (currentRevision != mediaPlayerController.playListUpdateRevision) { + onDownloadListChanged() + } + if (currentPlaying != mediaPlayerController.currentPlaying) { + onCurrentChanged() + } + onSliderProgressChanged() + requireActivity().invalidateOptionsMenu() + } + + private fun savePlaylistInBackground(playlistName: String) { + Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) + mediaPlayerController.suggestedPlaylistName = playlistName + object : SilentBackgroundTask(activity) { + @Throws(Throwable::class) + override fun doInBackground(): Void? { + val entries: MutableList = LinkedList() + for (downloadFile in mediaPlayerController.playList) { + entries.add(downloadFile.song) + } + val musicService = getMusicService() + musicService.createPlaylist(null, playlistName, entries) + return null + } + + override fun done(result: Void?) { + Util.toast(context, R.string.download_playlist_done) + } + + override fun error(error: Throwable) { + Timber.e(error, "Exception has occurred in savePlaylistInBackground") + val msg = String.format( + Locale.ROOT, + "%s %s", + resources.getString(R.string.download_playlist_error), + getErrorMessage(error) + ) + Util.toast(context, msg) + } + }.execute() + } + + private fun toggleFullScreenAlbumArt() { + if (playlistFlipper.displayedChild == 1) { + playlistFlipper.inAnimation = + AnimationUtils.loadAnimation(context, R.anim.push_down_in) + playlistFlipper.outAnimation = + AnimationUtils.loadAnimation(context, R.anim.push_down_out) + playlistFlipper.displayedChild = 0 + } else { + playlistFlipper.inAnimation = + AnimationUtils.loadAnimation(context, R.anim.push_up_in) + playlistFlipper.outAnimation = + AnimationUtils.loadAnimation(context, R.anim.push_up_out) + playlistFlipper.displayedChild = 1 + } + scrollToCurrent() + } + + private fun start() { + val service = mediaPlayerController + val state = service.playerState + if (state === PlayerState.PAUSED || + state === PlayerState.COMPLETED || state === PlayerState.STOPPED + ) { + service.start() + } else if (state === PlayerState.IDLE) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + val current = mediaPlayerController.currentPlayingNumberOnPlaylist + if (current == -1) { + service.play(0) + } else { + service.play(current) + } + } + } + + private fun onDownloadListChanged() { + val mediaPlayerController = mediaPlayerController + val list = mediaPlayerController.playList + emptyTextView.setText(R.string.download_empty) + val adapter = SongListAdapter(context, list) + playlistView.adapter = adapter + playlistView.setDragSortListener(object : DragSortListener { + override fun drop(from: Int, to: Int) { + if (from != to) { + val item = adapter.getItem(from) + adapter.remove(item) + adapter.notifyDataSetChanged() + adapter.insert(item, to) + adapter.notifyDataSetChanged() + } + } + + override fun drag(from: Int, to: Int) {} + override fun remove(which: Int) { + + val item = adapter.getItem(which) ?: return + + val currentPlaying = mediaPlayerController.currentPlaying + if (currentPlaying == item) { + mediaPlayerController.next() + } + adapter.remove(item) + adapter.notifyDataSetChanged() + val songRemoved = String.format( + resources.getString(R.string.download_song_removed), + item.song.title + ) + Util.toast(context, songRemoved) + onDownloadListChanged() + onCurrentChanged() + } + }) + emptyTextView.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE + currentRevision = mediaPlayerController.playListUpdateRevision + when (mediaPlayerController.repeatMode) { + RepeatMode.OFF -> repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + context, R.attr.media_repeat_off + ) + ) + RepeatMode.ALL -> repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + context, R.attr.media_repeat_all + ) + ) + RepeatMode.SINGLE -> repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + context, R.attr.media_repeat_single + ) + ) + else -> { + } + } + } + + private fun onCurrentChanged() { + currentPlaying = mediaPlayerController.currentPlaying + scrollToCurrent() + val totalDuration = mediaPlayerController.playListDuration + val totalSongs = mediaPlayerController.playlistSize.toLong() + val currentSongIndex = mediaPlayerController.currentPlayingNumberOnPlaylist + 1 + val duration = Util.formatTotalDuration(totalDuration) + val trackFormat = + String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs) + if (currentPlaying != null) { + currentSong = currentPlaying!!.song + songTitleTextView.text = currentSong!!.title + albumTextView.text = currentSong!!.album + artistTextView.text = currentSong!!.artist + downloadTrackTextView.text = trackFormat + downloadTotalDurationTextView.text = duration + imageLoaderProvider.getImageLoader() + .loadImage(albumArtImageView, currentSong, true, 0) + displaySongRating() + } else { + currentSong = null + songTitleTextView.text = null + albumTextView.text = null + artistTextView.text = null + downloadTrackTextView.text = null + downloadTotalDurationTextView.text = null + imageLoaderProvider.getImageLoader() + .loadImage(albumArtImageView, null, true, 0) + } + } + + private fun onSliderProgressChanged() { + if (onProgressChangedTask != null) { + return + } + onProgressChangedTask = object : SilentBackgroundTask(activity) { + var isJukeboxEnabled = false + var millisPlayed = 0 + var duration: Int? = null + var playerState: PlayerState? = null + override fun doInBackground(): Void? { + isJukeboxEnabled = mediaPlayerController.isJukeboxEnabled + millisPlayed = max(0, mediaPlayerController.playerPosition) + duration = mediaPlayerController.playerDuration + playerState = mediaPlayerController.playerState + return null + } + + @Suppress("LongMethod") + override fun done(result: Void?) { + if (cancellationToken.isCancellationRequested) return + if (currentPlaying != null) { + val millisTotal = if (duration == null) 0 else duration!! + positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) + durationTextView.text = Util.formatTotalDuration(millisTotal.toLong(), true) + progressBar.max = + if (millisTotal == 0) 100 else millisTotal // Work-around for apparent bug. + progressBar.progress = millisPlayed + progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled + } else { + positionTextView.setText(R.string.util_zero_time) + durationTextView.setText(R.string.util_no_time) + progressBar.progress = 0 + progressBar.max = 0 + progressBar.isEnabled = false + } + + when (playerState) { + PlayerState.DOWNLOADING -> { + val progress = + if (currentPlaying != null) currentPlaying!!.progress.value!! else 0 + val downloadStatus = resources.getString( + R.string.download_playerstate_downloading, + Util.formatPercentage(progress) + ) + setTitle(this@PlayerFragment, downloadStatus) + } + PlayerState.PREPARING -> setTitle( + this@PlayerFragment, + R.string.download_playerstate_buffering + ) + PlayerState.STARTED -> { + if (mediaPlayerController.isShufflePlayEnabled) { + setTitle( + this@PlayerFragment, + R.string.download_playerstate_playing_shuffle + ) + } else { + setTitle(this@PlayerFragment, R.string.common_appname) + } + } + PlayerState.IDLE, + PlayerState.PREPARED, + PlayerState.STOPPED, + PlayerState.PAUSED, + PlayerState.COMPLETED -> { + } + else -> setTitle(this@PlayerFragment, R.string.common_appname) + } + + when (playerState) { + PlayerState.STARTED -> { + pauseButton.visibility = View.VISIBLE + stopButton.visibility = View.GONE + startButton.visibility = View.GONE + } + PlayerState.DOWNLOADING, PlayerState.PREPARING -> { + pauseButton.visibility = View.GONE + stopButton.visibility = View.VISIBLE + startButton.visibility = View.GONE + } + else -> { + pauseButton.visibility = View.GONE + stopButton.visibility = View.GONE + startButton.visibility = View.VISIBLE + } + } + + // TODO: It would be a lot nicer if MediaPlayerController would send an event + // when this is necessary instead of updating every time + displaySongRating() + onProgressChangedTask = null + } + } + onProgressChangedTask!!.execute() + } + + private fun changeProgress(ms: Int) { + object : SilentBackgroundTask(activity) { + var msPlayed = 0 + var duration: Int? = null + var seekTo = 0 + override fun doInBackground(): Void? { + msPlayed = max(0, mediaPlayerController.playerPosition) + duration = mediaPlayerController.playerDuration + val msTotal = duration!! + seekTo = (msPlayed + ms).coerceAtMost(msTotal) + mediaPlayerController.seekTo(seekTo) + return null + } + + override fun done(result: Void?) { + progressBar.progress = seekTo + } + }.execute() + } + + override fun onDown(me: MotionEvent): Boolean { + return false + } + + @Suppress("ReturnCount") + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + val e1X = e1.x + val e2X = e2.x + val e1Y = e1.y + val e2Y = e2.y + val absX = abs(velocityX) + val absY = abs(velocityY) + + // Right to Left swipe + if (e1X - e2X > swipeDistance && absX > swipeVelocity) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.next() + onCurrentChanged() + onSliderProgressChanged() + return true + } + + // Left to Right swipe + if (e2X - e1X > swipeDistance && absX > swipeVelocity) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.previous() + onCurrentChanged() + onSliderProgressChanged() + return true + } + + // Top to Bottom swipe + if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) + onSliderProgressChanged() + return true + } + + // Bottom to Top swipe + if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) + onSliderProgressChanged() + return true + } + return false + } + + override fun onLongPress(e: MotionEvent) {} + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + return false + } + + override fun onShowPress(e: MotionEvent) {} + override fun onSingleTapUp(e: MotionEvent): Boolean { + return false + } + + private fun displaySongRating() { + var rating = 0 + + if (currentSong?.userRating != null) { + rating = currentSong!!.userRating!! + } + + fiveStar1ImageView.setImageDrawable(if (rating > 0) fullStar else hollowStar) + fiveStar2ImageView.setImageDrawable(if (rating > 1) fullStar else hollowStar) + fiveStar3ImageView.setImageDrawable(if (rating > 2) fullStar else hollowStar) + fiveStar4ImageView.setImageDrawable(if (rating > 3) fullStar else hollowStar) + fiveStar5ImageView.setImageDrawable(if (rating > 4) fullStar else hollowStar) + } + + private fun setSongRating(rating: Int) { + if (currentSong == null) return + displaySongRating() + mediaPlayerController.setSongRating(rating) + } + + private fun showSavePlaylistDialog() { + val layout = LayoutInflater.from(this.context).inflate(R.layout.save_playlist, null) + + playlistNameView = layout.findViewById(R.id.save_playlist_name) + + val builder: AlertDialog.Builder = AlertDialog.Builder(context) + builder.setTitle(R.string.download_playlist_title) + builder.setMessage(R.string.download_playlist_name) + + builder.setPositiveButton(R.string.common_save) { _, _ -> + savePlaylistInBackground( + playlistNameView.text.toString() + ) + } + + builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.cancel() } + builder.setView(layout) + builder.setCancelable(true) + val dialog = builder.create() + val playlistName = mediaPlayerController.suggestedPlaylistName + if (playlistName != null) { + playlistNameView.setText(playlistName) + } else { + val dateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + playlistNameView.setText(dateFormat.format(Date())) + } + dialog.show() + } + + companion object { + private const val PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 6e329d39..f18a0f26 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -198,7 +198,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun createPlaylist(id: String, name: String, entries: List) { + override fun createPlaylist(id: String?, name: String?, entries: List) { cachedPlaylists.clear() musicService.createPlaylist(id, name, entries) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 9216b3bd..1de7bac3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -20,6 +20,7 @@ import android.os.Looper import android.os.PowerManager import android.os.PowerManager.PARTIAL_WAKE_LOCK import android.os.PowerManager.WakeLock +import androidx.lifecycle.MutableLiveData import java.io.File import java.net.URLEncoder import java.util.Locale @@ -29,7 +30,6 @@ import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.fragment.PlayerFragment import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.StreamProxy @@ -79,10 +79,12 @@ class LocalMediaPlayer( private var proxy: StreamProxy? = null private var bufferTask: CancellableTask? = null private var positionCache: PositionCache? = null - private var secondaryProgress = -1 + private val pm = context.getSystemService(POWER_SERVICE) as PowerManager private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name) + val secondaryProgress: MutableLiveData = MutableLiveData(0) + fun init() { Thread { Thread.currentThread().name = "MediaPlayerThread" @@ -361,7 +363,6 @@ class LocalMediaPlayer( downloadFile.updateModificationDate() mediaPlayer.setOnCompletionListener(null) - secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works setPlayerState(PlayerState.IDLE) setAudioAttributes(mediaPlayer) @@ -392,28 +393,28 @@ class LocalMediaPlayer( setPlayerState(PlayerState.PREPARING) mediaPlayer.setOnBufferingUpdateListener { mp, percent -> - val progressBar = PlayerFragment.getProgressBar() val song = downloadFile.song if (percent == 100) { mp.setOnBufferingUpdateListener(null) } - secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt() - + // The secondary progress is an indicator of how far the song is cached. if (song.transcodedContentType == null && Util.getMaxBitRate() == 0) { - progressBar?.secondaryProgress = secondaryProgress + val progress = (percent.toDouble() / 100.toDouble() * playerDuration).toInt() + secondaryProgress.postValue(progress) } } mediaPlayer.setOnPreparedListener { Timber.i("Media player prepared") setPlayerState(PlayerState.PREPARED) - val progressBar = PlayerFragment.getProgressBar() - if (progressBar != null && downloadFile.isWorkDone) { - // Populate seek bar secondary progress if we have a complete file for consistency - PlayerFragment.getProgressBar().secondaryProgress = 100 * progressBar.max + + // Populate seek bar secondary progress if we have a complete file for consistency + if (downloadFile.isWorkDone) { + secondaryProgress.postValue(playerDuration) } + synchronized(this@LocalMediaPlayer) { if (position != 0) { Timber.i("Restarting player from position %d", position) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index cd0f16c6..b18eb0fa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -386,12 +386,6 @@ class MediaPlayerController( @get:Synchronized val playerDuration: Int get() { - if (localMediaPlayer.currentPlaying != null) { - val duration = localMediaPlayer.currentPlaying!!.song.duration - if (duration != null) { - return duration * 1000 - } - } val mediaPlayerService = runningInstance ?: return 0 return mediaPlayerService.playerDuration } @@ -454,6 +448,19 @@ class MediaPlayerController( if (localMediaPlayer.currentPlaying == null) return val song = localMediaPlayer.currentPlaying!!.song + Thread { + val musicService = getMusicService() + try { + if (song.starred) { + musicService.unstar(song.id, null, null) + } else { + musicService.star(song.id, null, null) + } + } catch (all: Exception) { + Timber.e(all) + } + }.start() + // Trigger an update localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) song.starred = !song.starred diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index cce41209..6e417358 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -73,7 +73,7 @@ interface MusicService { fun getPlaylists(refresh: Boolean): List @Throws(Exception::class) - fun createPlaylist(id: String, name: String, entries: List) + fun createPlaylist(id: String?, name: String?, entries: List) @Throws(Exception::class) fun deletePlaylist(id: String) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index f8519561..e06a3a22 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -221,7 +221,7 @@ class OfflineMusicService : MusicService, KoinComponent { @Suppress("TooGenericExceptionCaught") @Throws(Exception::class) - override fun createPlaylist(id: String, name: String, entries: List) { + override fun createPlaylist(id: String?, name: String?, entries: List) { val playlistFile = FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name) val fw = FileWriter(playlistFile) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 4876bd9e..6e0e44e1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -295,12 +295,20 @@ open class RESTMusicService( return response.body()!!.playlists.toDomainEntitiesList() } + /** + * Either ID or String is required. + * ID is required when updating + * String is required when creating + */ @Throws(Exception::class) override fun createPlaylist( - id: String, - name: String, + id: String?, + name: String?, entries: List ) { + if (id == null && name == null) + throw IllegalArgumentException("Either id or name is required.") + val pSongIds: MutableList = ArrayList(entries.size) for ((id1) in entries) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt new file mode 100644 index 00000000..3639aa2c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt @@ -0,0 +1,32 @@ +/* + * SilentBackgroundTask.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import android.app.Activity + +/** + * @author Sindre Mehus + */ +abstract class SilentBackgroundTask(activity: Activity?) : BackgroundTask(activity) { + override fun execute() { + val thread: Thread = object : Thread() { + override fun run() { + try { + val result = doInBackground() + handler.post { done(result) } + } catch (all: Throwable) { + handler.post { error(all) } + } + } + } + thread.start() + } + + override fun updateProgress(messageId: Int) {} + override fun updateProgress(message: String) {} +}