Java tutorial
/* * Copyright (C) 2010-2014 The MPDroid Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.audiokernel.euphonyrmt.fragments; import com.audiokernel.euphonyrmt.MPDApplication; import com.audiokernel.euphonyrmt.R; import com.audiokernel.euphonyrmt.helpers.AlbumCoverDownloadListener; import com.audiokernel.euphonyrmt.helpers.AlbumInfo; import com.audiokernel.euphonyrmt.helpers.CoverAsyncHelper; import com.audiokernel.euphonyrmt.helpers.CoverManager; import com.audiokernel.euphonyrmt.helpers.MPDControl; import com.audiokernel.euphonyrmt.helpers.UpdateTrackInfo; import com.audiokernel.euphonyrmt.library.SimpleLibraryActivity; import org.a0z.mpd.MPDCommand; import org.a0z.mpd.MPDStatus; import org.a0z.mpd.Tools; import org.a0z.mpd.event.StatusChangeListener; import org.a0z.mpd.event.TrackPositionListener; import org.a0z.mpd.exception.MPDException; import org.a0z.mpd.item.Music; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.AttrRes; import android.support.annotation.IdRes; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.widget.PopupMenuCompat; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.ProgressBar; import android.widget.RatingBar; import android.widget.SeekBar; import android.widget.TextView; import java.io.IOException; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class NowPlayingFragment extends Fragment implements StatusChangeListener, TrackPositionListener, OnSharedPreferenceChangeListener, OnMenuItemClickListener, UpdateTrackInfo.FullTrackInfoUpdate { /** In milliseconds. */ private static final long ANIMATION_DURATION = 1000L; private static final int POPUP_ALBUM = 2; private static final int POPUP_ALBUM_ARTIST = 1; private static final int POPUP_ARTIST = 0; private static final int POPUP_COVER_BLACKLIST = 7; private static final int POPUP_COVER_SELECTIVE_CLEAN = 8; private static final int POPUP_CURRENT = 6; private static final int POPUP_FOLDER = 3; private static final int POPUP_SHARE = 5; private static final int POPUP_STREAM = 4; private static final String TAG = "NowPlayingFragment"; private final MPDApplication mApp = MPDApplication.getInstance(); private final Timer mVolTimer = new Timer(); private FragmentActivity mActivity; private TextView mAlbumNameText; private TextView mArtistNameText; private TextView mAudioNameText = null; private ImageView mCoverArt; private CoverAsyncHelper mCoverAsyncHelper = null; private AlbumCoverDownloadListener mCoverDownloadListener; private Music mCurrentSong = null; private Handler mHandler; private boolean mIsAudioNameTextEnabled = false; private ImageButton mPlayPauseButton = null; private View.OnTouchListener mPopupMenuStreamTouchListener = null; private View.OnTouchListener mPopupMenuTouchListener = null; private Timer mPosTimer = null; private ImageButton mRepeatButton = null; private SharedPreferences mSharedPreferences; private ImageButton mShuffleButton = null; private View mSongInfo = null; private TextView mSongNameText; private RatingBar mSongRating = null; private ImageButton mStopButton = null; private SeekBar mTrackSeekBar = null; private TextView mTrackTime = null; private TextView mTrackTotalTime = null; private TimerTask mVolTimerTask = null; private ImageView mVolumeIcon = null; private SeekBar mVolumeSeekBar = null; private TextView mYearNameText; /** * A convenience method to find a resource and set it as selected. * * @param view The view to find the resource in. * @param resource The resource to find in the view. * @return The TextView found in the {@code view}, set as selected. */ private static TextView findSelected(final View view, @IdRes final int resource) { final TextView textView = (TextView) view.findViewById(resource); textView.setSelected(true); return textView; } /** * This method sets up a resource with the button event handler. * * @param view The {@code View} with which to setup the {@code ImageButton}. * @param resource The resource to find in the view. * @param longPress Whether long press is supported by this event button. * @return The generated {@code ImageButton}. */ private static ImageButton getEventButton(final View view, @IdRes final int resource, final boolean longPress) { final ImageButton button = (ImageButton) view.findViewById(resource); final ButtonEventHandler buttonEventHandler = new ButtonEventHandler(); button.setOnClickListener(buttonEventHandler); if (longPress) { button.setOnLongClickListener(buttonEventHandler); } return button; } protected static int getPlayPauseResource(final int state) { final int resource; if (MPDApplication.getInstance().isLightThemeSelected()) { if (state == MPDStatus.STATE_PLAYING) { resource = R.drawable.ic_media_pause_light; } else { resource = R.drawable.ic_media_play_light; } } else { if (state == MPDStatus.STATE_PLAYING) { resource = R.drawable.ic_media_pause; } else { resource = R.drawable.ic_media_play; } } return resource; } /** * Retrieve styled attribute information for the repeat button. * * @param on True if repeat is enabled, false otherwise. * @return Returns the enabled repeat styled attribute if on, returns the disabled repeat styled * attribute otherwise. */ private static int getRepeatAttribute(final boolean on) { final int attribute; if (on) { attribute = R.attr.repeatEnabled; } else { attribute = R.attr.repeatDisabled; } return attribute; } /** * Retrieve styled attribute information for the random button. * * @param on True if random is enabled, false otherwise. * @return Returns the enabled random styled attribute if on, returns the disabled random styled * attribute otherwise. */ private static int getShuffleAttribute(final boolean on) { final int attribute; if (on) { attribute = R.attr.shuffleEnabled; } else { attribute = R.attr.shuffleDisabled; } return attribute; } /** * Generates the initial track {@link android.widget.SeekBar}. * * @param view The view in which to setup the {@code SeekBar} for. * @return The constructed SeekBar for the track position modification. */ private static SeekBar getTrackSeekBar(final View view) { final SeekBar.OnSeekBarChangeListener seekBarTrackListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) { } @Override public void onStartTrackingTouch(final SeekBar seekBar) { } @Override public void onStopTrackingTouch(final SeekBar seekBar) { MPDControl.run(MPDControl.ACTION_SEEK, seekBar.getProgress()); } }; final SeekBar seekBarTrack = (SeekBar) view.findViewById(R.id.progress_track); seekBarTrack.setOnSeekBarChangeListener(seekBarTrackListener); return seekBarTrack; } private void applyViewVisibility(final View view, final String property) { if (mSharedPreferences.getBoolean(property, false)) { view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } @Override public void connectionStateChanged(final boolean connected, final boolean connectionLost) { if (connected) { forceStatusUpdate(); } else { mSongNameText.setText(R.string.notConnected); } } private void downloadCover(final AlbumInfo albumInfo) { mCoverAsyncHelper.downloadCover(albumInfo, true); } private void forceStatusUpdate() { final MPDStatus status = mApp.oMPDAsyncHelper.oMPD.getStatus(); if (status.isValid()) { volumeChanged(status, -1); updateStatus(status); updateTrackInfo(status, true); setButtonAttribute(getRepeatAttribute(status.isRepeat()), mRepeatButton); setButtonAttribute(getShuffleAttribute(status.isRandom()), mShuffleButton); setStickerVisibility(); } } /** * Run during fragment initialization, this sets up the cover art popup menu and the coverArt * ImageView. * * @param view The view to setup the coverArt ImageView in. * @return The resulting ImageView. */ private ImageView getCoverArt(final View view) { final ImageView coverArt = (ImageView) view.findViewById(R.id.albumCover); final PopupMenu coverMenu = new PopupMenu(mActivity, coverArt); final Menu menu = coverMenu.getMenu(); coverArt.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { scrollToNowPlaying(); } }); menu.add(Menu.NONE, POPUP_COVER_BLACKLIST, Menu.NONE, R.string.otherCover); menu.add(Menu.NONE, POPUP_COVER_SELECTIVE_CLEAN, Menu.NONE, R.string.resetCover); coverMenu.setOnMenuItemClickListener(this); coverArt.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { final boolean isConsumed; if (mCurrentSong != null) { menu.setGroupVisible(Menu.NONE, new AlbumInfo(mCurrentSong).isValid()); coverMenu.show(); isConsumed = true; } else { isConsumed = false; } return isConsumed; } }); return coverArt; } /** * This sets up the {@code CoverAsyncHelper} for this class. * * @param view The view in which to setup the {@code CoverAsyncHelper} for. * @return The CoverAsyncHelper used as a field in this class. */ private CoverAsyncHelper getCoverAsyncHelper(final View view) { final CoverAsyncHelper coverAsyncHelper = new CoverAsyncHelper(); final ProgressBar coverArtProgress = (ProgressBar) view.findViewById(R.id.albumCoverProgress); // Scale cover images down to screen width coverAsyncHelper.setCoverMaxSizeFromScreen(mActivity); coverAsyncHelper.setCachedCoverMaxSize(mCoverArt.getWidth()); mCoverDownloadListener = new AlbumCoverDownloadListener(mCoverArt, coverArtProgress, true); coverAsyncHelper.addCoverDownloadListener(mCoverDownloadListener); return coverAsyncHelper; } /** * This method generates selected track information to send to another application. * * The current format of this method should output: * header artist - title and if the output is a stream, the URL should be suffixed on the end. * * @return The track information to send to another application. */ private String getShareString() { final char[] separator = { ' ', '-', ' ' }; final String fullPath = mCurrentSong.getFullPath(); final String sharePrefix = getString(R.string.sharePrefix); final String trackArtist = mCurrentSong.getArtist(); final String trackTitle = mCurrentSong.getTitle(); final int initialLength = trackTitle.length() + sharePrefix.length() + 64; final StringBuilder shareString = new StringBuilder(initialLength); shareString.append(sharePrefix); shareString.append(' '); if (trackArtist != null) { shareString.append(trackArtist); shareString.append(separator); } shareString.append(trackTitle); /** If track title is empty, the full path will have been substituted.*/ if (mCurrentSong.isStream() && !fullPath.startsWith(trackTitle)) { shareString.append(separator); shareString.append(fullPath); } return shareString.toString(); } /** * Run during fragment initialization, this sets up the song info popup menu. * * @param view The view in which to setup the song info View for this class. * @return The song info view used as a field in this class. */ private View getSongInfo(final View view) { final View songInfo = view.findViewById(R.id.songInfo); final PopupMenu popupMenu = new PopupMenu(mActivity, songInfo); final Menu menu = popupMenu.getMenu(); menu.add(Menu.NONE, POPUP_ALBUM, Menu.NONE, R.string.goToAlbum); menu.add(Menu.NONE, POPUP_ARTIST, Menu.NONE, R.string.goToArtist); menu.add(Menu.NONE, POPUP_ALBUM_ARTIST, Menu.NONE, R.string.goToAlbumArtist); menu.add(Menu.NONE, POPUP_FOLDER, Menu.NONE, R.string.goToFolder); menu.add(Menu.NONE, POPUP_CURRENT, Menu.NONE, R.string.goToCurrent); menu.add(Menu.NONE, POPUP_SHARE, Menu.NONE, R.string.share); popupMenu.setOnMenuItemClickListener(this); mPopupMenuTouchListener = PopupMenuCompat.getDragToOpenListener(popupMenu); final PopupMenu popupMenuStream = new PopupMenu(mActivity, songInfo); final Menu menuStream = popupMenuStream.getMenu(); menuStream.add(Menu.NONE, POPUP_STREAM, Menu.NONE, R.string.goToStream); menuStream.add(Menu.NONE, POPUP_CURRENT, Menu.NONE, R.string.goToCurrent); menuStream.add(Menu.NONE, POPUP_SHARE, Menu.NONE, R.string.share); popupMenuStream.setOnMenuItemClickListener(this); mPopupMenuStreamTouchListener = PopupMenuCompat.getDragToOpenListener(popupMenuStream); songInfo.setOnClickListener(new OnClickListener() { /** * Checks whether the album artist should be on the popup menu for the current track. * * @return True if the album artist popup menu entry should be visible, false otherwise. */ private boolean isAlbumArtistVisible() { boolean albumArtistEnabled = false; final String albumArtist = mCurrentSong.getAlbumArtist(); if (albumArtist != null && !albumArtist.isEmpty()) { final String artist = mCurrentSong.getArtist(); if (isArtistVisible() && !albumArtist.equals(artist)) { albumArtistEnabled = true; } } return albumArtistEnabled; } /** * Checks whether the album should be on the popup menu for the current track. * * @return True if the album popup menu entry should be visible, false otherwise. */ private boolean isAlbumVisible() { final boolean isAlbumVisible; final String album = mCurrentSong.getAlbum(); if (album != null && !album.isEmpty()) { isAlbumVisible = true; } else { isAlbumVisible = false; } return isAlbumVisible; } /** * Checks whether the artist should be on the popup menu for the current track. * * @return True if the artist popup menu entry should be visible, false otherwise. */ private boolean isArtistVisible() { final boolean isArtistVisible; final String artist = mCurrentSong.getArtist(); if (artist != null && !artist.isEmpty()) { isArtistVisible = true; } else { isArtistVisible = false; } return isArtistVisible; } /** * This method checks the dynamic entries for visibility prior to showing the song info * popup menu. * * @param v The view for the song info popup menu. */ @Override public void onClick(final View v) { if (mCurrentSong != null) { if (mCurrentSong.isStream()) { popupMenuStream.show(); } else { // Enable / Disable menu items that need artist and album defined. menu.findItem(POPUP_ALBUM).setVisible(isAlbumVisible()); menu.findItem(POPUP_ARTIST).setVisible(isArtistVisible()); menu.findItem(POPUP_ALBUM_ARTIST).setVisible(isAlbumArtistVisible()); popupMenu.show(); } } } }); return songInfo; } private float getTrackRating() { float rating = 0.0f; try { rating = (float) mApp.oMPDAsyncHelper.oMPD.getStickerManager().getRating(mCurrentSong); } catch (final IOException | MPDException e) { Log.e(TAG, "Failed to get the current track rating.", e); } return rating / 2.0f; } /** * Generates the volume {@link android.widget.SeekBar}. * * @param view The view in which to setup the {@code SeekBar} for. * @return The constructed SeekBar for the volume modification. */ private SeekBar getVolumeSeekBar(final View view) { final SeekBar.OnSeekBarChangeListener seekBarListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) { } @Override public void onStartTrackingTouch(final SeekBar seekBar) { mVolTimerTask = new TimerTask() { private int mLastSentVol = -1; private SeekBar mProgress; @Override public void run() { final int progress = mProgress.getProgress(); if (mLastSentVol != progress) { mLastSentVol = progress; MPDControl.run(MPDControl.ACTION_VOLUME_SET, progress); } } public TimerTask setProgress(final SeekBar prg) { mProgress = prg; return this; } }.setProgress(seekBar); mVolTimer.scheduleAtFixedRate(mVolTimerTask, (long) MPDCommand.MIN_VOLUME, (long) MPDCommand.MAX_VOLUME); } @Override public void onStopTrackingTouch(final SeekBar seekBar) { mVolTimerTask.cancel(); mVolTimerTask.run(); } }; final SeekBar volumeSeekBar = (SeekBar) view.findViewById(R.id.progress_volume); volumeSeekBar.setOnSeekBarChangeListener(seekBarListener); return volumeSeekBar; } /** * This method handles any simple library activity item ids. * * @param itemId The itemId to attempt to handle. * @return {@code true} if this is handled by a simple library activity, * {@code false} otherwise. */ private boolean isSimpleLibraryItem(final int itemId) { Intent intent = null; switch (itemId) { case POPUP_ALBUM: case POPUP_ALBUM_ARTIST: case POPUP_ARTIST: case POPUP_FOLDER: if (mCurrentSong != null) { intent = simpleLibraryMusicItem(itemId); } break; case POPUP_STREAM: intent = new Intent(mActivity, SimpleLibraryActivity.class); intent.putExtra("streams", true); break; default: break; } if (intent != null) { /** * Set the result for SimpleLibraryActivity to * return so getCallingActivity() will work. */ startActivityForResult(intent, 1); } return intent != null; } /** * This method handles any simple library activity item ids which handle music items. * * @param itemId The itemId to attempt to handle. * @return An intent to start the {@link com.audiokernel.euphonyrmt.library.SimpleLibraryActivity}. */ private Intent simpleLibraryMusicItem(final int itemId) { final Intent intent = new Intent(mActivity, SimpleLibraryActivity.class); switch (itemId) { case POPUP_ALBUM: intent.putExtra("album", mCurrentSong.getAlbumAsAlbum()); break; case POPUP_ALBUM_ARTIST: intent.putExtra("artist", mCurrentSong.getAlbumArtistAsArtist()); break; case POPUP_ARTIST: intent.putExtra("artist", mCurrentSong.getArtistAsArtist()); break; case POPUP_FOLDER: final String path = mCurrentSong.getFullPath(); final String parent = mCurrentSong.getParent(); if (path == null || parent == null) { break; } intent.putExtra("folder", parent); break; default: break; } return intent; } @Override public void libraryStateChanged(final boolean updating, final boolean dbChanged) { } @Override public void onAttach(final Activity activity) { super.onAttach(activity); mActivity = (FragmentActivity) activity; } /** * This is called when cover art needs to be updated due to server information change. * * @param albumInfo The current albumInfo */ @Override public final void onCoverUpdate(final AlbumInfo albumInfo) { final int noCoverResource = AlbumCoverDownloadListener.getLargeNoCoverResource(); mCoverArt.setImageResource(noCoverResource); if (albumInfo != null) { downloadCover(albumInfo); } } @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new Handler(); setHasOptionsMenu(false); mActivity.setTitle(R.string.nowPlaying); } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); final Animation fadeIn = AnimationUtils.loadAnimation(mActivity, android.R.anim.fade_in); final Animation fadeOut = AnimationUtils.loadAnimation(mActivity, android.R.anim.fade_out); final int viewLayout; final View view; mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity); mSharedPreferences.registerOnSharedPreferenceChangeListener(this); if (mApp.isTabletUiEnabled()) { viewLayout = R.layout.main_fragment_tablet; } else { viewLayout = R.layout.main_fragment; } view = inflater.inflate(viewLayout, container, false); mTrackTime = (TextView) view.findViewById(R.id.trackTime); mTrackTotalTime = (TextView) view.findViewById(R.id.trackTotalTime); mVolumeIcon = (ImageView) view.findViewById(R.id.volume_icon); /** These load the TextView resource, and set it as selected. */ mAlbumNameText = findSelected(view, R.id.albumName); mArtistNameText = findSelected(view, R.id.artistName); mAudioNameText = findSelected(view, R.id.audioName); mSongNameText = findSelected(view, R.id.songName); mSongNameText.setText(R.string.notConnected); mYearNameText = findSelected(view, R.id.yearName); applyViewVisibility(mYearNameText, "enableAlbumYearText"); mSongRating = (RatingBar) view.findViewById(R.id.songRating); mSongRating.setOnRatingBarChangeListener(new RatingChangedHandler()); mSongRating.setVisibility(View.GONE); /** These get the event button, then setup listeners for them. */ mPlayPauseButton = getEventButton(view, R.id.playpause, true); mRepeatButton = getEventButton(view, R.id.repeat, false); mShuffleButton = getEventButton(view, R.id.shuffle, false); mStopButton = getEventButton(view, R.id.stop, true); applyViewVisibility(mStopButton, "enableStopButton"); /** Same as above, but these don't require a stored field. */ getEventButton(view, R.id.next, false); getEventButton(view, R.id.prev, false); /** These have methods to initialize everything required to get them setup. */ mCoverArt = getCoverArt(view); mCoverAsyncHelper = getCoverAsyncHelper(view); mSongInfo = getSongInfo(view); mTrackSeekBar = getTrackSeekBar(view); mVolumeSeekBar = getVolumeSeekBar(view); fadeIn.setDuration(ANIMATION_DURATION); fadeOut.setDuration(ANIMATION_DURATION); Log.i(TAG, "Initialization succeeded"); return view; } @Override public void onDestroy() { final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); settings.unregisterOnSharedPreferenceChangeListener(this); super.onDestroy(); } @Override public void onDestroyView() { mCoverArt.setImageResource(AlbumCoverDownloadListener.getNoCoverResource()); mCoverDownloadListener.freeCoverDrawable(); super.onDestroyView(); } @Override public void onDetach() { super.onDetach(); mActivity = null; } @Override public boolean onMenuItemClick(final MenuItem item) { final AlbumInfo albumInfo; boolean result = true; final int itemId = item.getItemId(); switch (item.getItemId()) { case POPUP_COVER_BLACKLIST: albumInfo = new AlbumInfo(mCurrentSong); CoverManager.getInstance().markWrongCover(albumInfo); downloadCover(albumInfo); updateQueueCovers(albumInfo); break; case POPUP_COVER_SELECTIVE_CLEAN: albumInfo = new AlbumInfo(mCurrentSong); CoverManager.getInstance().clear(albumInfo); downloadCover(albumInfo); updateQueueCovers(albumInfo); break; case POPUP_CURRENT: scrollToNowPlaying(); break; case POPUP_SHARE: final Intent intent = new Intent(Intent.ACTION_SEND, null); intent.putExtra(Intent.EXTRA_TEXT, getShareString()); intent.setType("text/plain"); startActivity(intent); break; default: result = isSimpleLibraryItem(itemId); break; } return result; } @Override public void onResume() { super.onResume(); final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); mIsAudioNameTextEnabled = settings.getBoolean("enableAudioText", false); forceStatusUpdate(); } @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { switch (key) { case CoverManager.PREFERENCE_CACHE: case CoverManager.PREFERENCE_LASTFM: case CoverManager.PREFERENCE_LOCALSERVER: CoverAsyncHelper.setCoverRetrieversFromPreferences(); break; case "enableStopButton": applyViewVisibility(mStopButton, key); break; case "enableAlbumYearText": applyViewVisibility(mYearNameText, key); break; case "enableAudioText": mIsAudioNameTextEnabled = sharedPreferences.getBoolean(key, false); updateAudioNameText(mApp.oMPDAsyncHelper.oMPD.getStatus()); break; case "enableRating": setStickerVisibility(); updateTrackInfo(mApp.oMPDAsyncHelper.oMPD.getStatus(), false); break; default: break; } } @Override public void onStart() { super.onStart(); if (mApp.updateTrackInfo == null) { mApp.updateTrackInfo = new UpdateTrackInfo(); } mApp.updateTrackInfo.addCallback(this); mApp.oMPDAsyncHelper.addStatusChangeListener(this); mApp.oMPDAsyncHelper.addTrackPositionListener(this); mApp.setActivity(this); } @Override public void onStop() { super.onStop(); mApp.updateTrackInfo.removeCallback(this); mApp.oMPDAsyncHelper.removeStatusChangeListener(this); mApp.oMPDAsyncHelper.removeTrackPositionListener(this); stopPosTimer(); mApp.unsetActivity(this); } /** * Called when a track information change has been detected. * * @param updatedSong The currentSong item object. * @param album The album change. * @param artist The artist change. * @param date The date change. * @param title The title change. */ @Override public final void onTrackInfoUpdate(final Music updatedSong, final float trackRating, final CharSequence album, final CharSequence artist, final CharSequence date, final CharSequence title) { mCurrentSong = updatedSong; mAlbumNameText.setText(album); mArtistNameText.setText(artist); mSongNameText.setText(title); mSongRating.setRating(trackRating); mYearNameText.setText(date); updateAudioNameText(mApp.oMPDAsyncHelper.oMPD.getStatus()); } @Override public void playlistChanged(final MPDStatus mpdStatus, final int oldPlaylistVersion) { /** * If the current song is a stream, the metadata can change in place, and that will only * change the playlist, not the track, so, update if we detect a stream. */ if (mCurrentSong != null && mCurrentSong.isStream() || mpdStatus.isState(MPDStatus.STATE_STOPPED)) { updateTrackInfo(mpdStatus, false); } } @Override public void randomChanged(final boolean random) { setButtonAttribute(getShuffleAttribute(random), mShuffleButton); } @Override public void repeatChanged(final boolean repeating) { setButtonAttribute(getRepeatAttribute(repeating), mRepeatButton); } private void scrollToNowPlaying() { final QueueFragment queueFragment = (QueueFragment) mActivity.getSupportFragmentManager() .findFragmentById(R.id.queue_fragment); if (queueFragment == null) { Log.w(TAG, "Queue fragment was not available when scrolling to playing track."); } else { queueFragment.scrollToNowPlaying(); } } /** * Sets a buttons attributes. * * @param attribute The attribute resource to set the button to. * @param button The button with which to set the attribute resource. */ private void setButtonAttribute(@AttrRes final int attribute, final ImageButton button) { final int[] attrs = { attribute }; final TypedArray ta = mActivity.obtainStyledAttributes(attrs); final Drawable drawableFromTheme = ta.getDrawable(0); button.setImageDrawable(drawableFromTheme); button.invalidate(); ta.recycle(); } private void setStickerVisibility() { if (mApp.oMPDAsyncHelper.oMPD.getStickerManager().isAvailable()) { applyViewVisibility(mSongRating, "enableRating"); } else { mSongRating.setVisibility(View.GONE); } } private void startPosTimer(final long start, final long total) { stopPosTimer(); mPosTimer = new Timer(); final TimerTask posTimerTask = new PosTimerTask(start, total); mPosTimer.scheduleAtFixedRate(posTimerTask, 0L, DateUtils.SECOND_IN_MILLIS); } @Override public void stateChanged(final MPDStatus mpdStatus, final int oldState) { if (mActivity != null) { updateStatus(mpdStatus); updateAudioNameText(mpdStatus); } } @Override public void stickerChanged(final MPDStatus mpdStatus) { if (mSongRating.getVisibility() == View.VISIBLE && mCurrentSong != null) { /** This track is not necessarily the track that was changed. */ final float rating = getTrackRating(); mSongRating.setRating(rating); } } private void stopPosTimer() { if (null != mPosTimer) { mPosTimer.cancel(); mPosTimer = null; } } /** * Toggle the track progress bar. This should be called only when the track changes, for * position changes, startPosTimer() is sufficient. * * @param status A current {@code MPDStatus} object. */ private void toggleTrackProgress(final MPDStatus status) { final long totalTime = status.getTotalTime(); if (totalTime == 0) { mSongRating.setVisibility(View.GONE); mTrackTime.setVisibility(View.INVISIBLE); mTrackTotalTime.setVisibility(View.INVISIBLE); stopPosTimer(); mTrackSeekBar.setProgress(0); mTrackSeekBar.setEnabled(false); } else { final long elapsedTime = status.getElapsedTime(); if (status.isState(MPDStatus.STATE_PLAYING)) { startPosTimer(elapsedTime, totalTime); } else { stopPosTimer(); updateTrackProgress(elapsedTime, totalTime); } mTrackSeekBar.setMax((int) totalTime); mTrackTime.setVisibility(View.VISIBLE); mTrackTotalTime.setVisibility(View.VISIBLE); mTrackSeekBar.setEnabled(true); } } /** * This enables or disables the volume, depending on the volume given by the server. * * @param volume The current volume value. */ private void toggleVolumeBar(final int volume) { if (volume < MPDCommand.MIN_VOLUME || volume > MPDCommand.MAX_VOLUME) { mVolumeSeekBar.setEnabled(false); mVolumeSeekBar.setVisibility(View.GONE); mVolumeIcon.setVisibility(View.GONE); } else { mVolumeSeekBar.setEnabled(true); mVolumeSeekBar.setVisibility(View.VISIBLE); mVolumeIcon.setVisibility(View.VISIBLE); } } @Override public void trackChanged(final MPDStatus mpdStatus, final int oldTrack) { updateTrackInfo(mpdStatus, false); } @Override public void trackPositionChanged(final MPDStatus status) { toggleTrackProgress(status); } /** * This will update the audioNameText (extra track information) field. * * @param status An {@code MPDStatus} object. */ private void updateAudioNameText(final MPDStatus status) { StringBuilder optionalTrackInfo = null; if (mCurrentSong != null && mIsAudioNameTextEnabled && !status.isState(MPDStatus.STATE_STOPPED)) { final char[] separator = { ' ', '|', ' ' }; final String fileExtension = Tools.getExtension(mCurrentSong.getFullPath()); final long bitRate = status.getBitrate(); final int bitsPerSample = status.getBitsPerSample(); final int sampleRate = status.getSampleRate(); optionalTrackInfo = new StringBuilder(40); /** * Check each individual bit of info, the sever can give * out empty (and buggy) information from time to time. */ if (fileExtension != null) { optionalTrackInfo.append(fileExtension.toUpperCase()); } /** The server can give out buggy (and empty) information from time to time. */ if (bitRate > 0L) { if (optionalTrackInfo.length() > 0) { optionalTrackInfo.append(separator); } optionalTrackInfo.append(bitRate); optionalTrackInfo.append("kbps"); } if (bitsPerSample > 0) { if (optionalTrackInfo.length() > 0) { optionalTrackInfo.append(separator); } optionalTrackInfo.append(bitsPerSample); optionalTrackInfo.append("bits"); } if (sampleRate > 1000) { if (optionalTrackInfo.length() > 0) { optionalTrackInfo.append(separator); } optionalTrackInfo.append(Math.abs(sampleRate / 1000.0f)); optionalTrackInfo.append("kHz"); } if (optionalTrackInfo.length() > 0) { mAudioNameText.setText(optionalTrackInfo); mAudioNameText.setVisibility(View.VISIBLE); } } if (optionalTrackInfo == null || optionalTrackInfo.length() == 0) { mAudioNameText.setVisibility(View.GONE); } } private void updateQueueCovers(final AlbumInfo albumInfo) { final QueueFragment queueFragment = (QueueFragment) mActivity.getSupportFragmentManager() .findFragmentById(R.id.queue_fragment); if (queueFragment == null) { Log.w(TAG, "Queue fragment was not available for cover update."); } else { queueFragment.updateCover(albumInfo); } } private void updateStatus(final MPDStatus status) { toggleTrackProgress(status); mPlayPauseButton.setImageResource(getPlayPauseResource(status.getState())); View.OnTouchListener currentListener = null; if (mCurrentSong != null) { if (mCurrentSong.isStream()) { currentListener = mPopupMenuStreamTouchListener; } else { currentListener = mPopupMenuTouchListener; } } mSongInfo.setOnTouchListener(currentListener); } private void updateTrackInfo(final MPDStatus status, final boolean forcedUpdate) { if (mApp.oMPDAsyncHelper.oMPD.isConnected() && isAdded()) { toggleTrackProgress(status); mApp.updateTrackInfo.refresh(status, forcedUpdate); } } /** * Update the track progress numbers and track {@code SeekBar} object. * * @param elapsed The current track elapsed time. * @param totalTrackTime The current track total time. */ private void updateTrackProgress(final long elapsed, final long totalTrackTime) { /** In case the total track time is flawed. */ final long elapsedTime; if (elapsed > totalTrackTime) { elapsedTime = totalTrackTime; } else { elapsedTime = elapsed; } mTrackSeekBar.setProgress((int) elapsedTime); mHandler.post(new Runnable() { @Override public void run() { mTrackTime.setText(Music.timeToString(elapsedTime)); mTrackTotalTime.setText(Music.timeToString(totalTrackTime)); } }); } @Override public void volumeChanged(final MPDStatus mpdStatus, final int oldVolume) { final int volume = mpdStatus.getVolume(); toggleVolumeBar(volume); mVolumeSeekBar.setProgress(volume); } private static class ButtonEventHandler implements OnClickListener, View.OnLongClickListener { @Override public void onClick(final View v) { MPDControl.run(v.getId()); } @Override public boolean onLongClick(final View v) { final boolean isConsumed; final MPDApplication app = MPDApplication.getInstance(); final MPDStatus mpdStatus = app.oMPDAsyncHelper.oMPD.getStatus(); if (v.getId() == R.id.playpause && !mpdStatus.isState(MPDStatus.STATE_STOPPED)) { MPDControl.run(MPDControl.ACTION_STOP); isConsumed = true; } else { isConsumed = false; } return isConsumed; } } /** * This class runs a timer to keep the time elapsed since last track elapsed time updated for * the purpose of keeping the track progress up to date without continual server polling. */ private class PosTimerTask extends TimerTask { private final long mTimerStartTime; private long mElapsedTime = 0L; private long mStartTrackTime = 0L; private long mTotalTrackTime = 0L; private PosTimerTask(final long start, final long total) { super(); mStartTrackTime = start; mTotalTrackTime = total; mTimerStartTime = new Date().getTime(); } @Override public void run() { final long elapsedSinceTimerStart = new Date().getTime() - mTimerStartTime; mElapsedTime = mStartTrackTime + elapsedSinceTimerStart / DateUtils.SECOND_IN_MILLIS; updateTrackProgress(mElapsedTime, mTotalTrackTime); } } private class RatingChangedHandler implements RatingBar.OnRatingBarChangeListener { @Override public void onRatingChanged(final RatingBar ratingBar, final float rating, final boolean fromUser) { final int trackRating = (int) rating * 2; if (fromUser && mCurrentSong != null) { try { mApp.oMPDAsyncHelper.oMPD.getStickerManager().setRating(mCurrentSong, trackRating); } catch (final IOException | MPDException e) { Log.e(TAG, "Failed to set the rating.", e); } Log.d(TAG, "Rating changed to " + rating); } } } }