diff options
-rw-r--r-- | res/layout/activity_player_fragment.xml | 12 | ||||
-rw-r--r-- | res/layout/album_art_fragment.xml | 16 | ||||
-rw-r--r-- | src/com/cyngn/eleven/IElevenService.aidl | 8 | ||||
-rw-r--r-- | src/com/cyngn/eleven/MusicPlaybackService.java | 301 | ||||
-rw-r--r-- | src/com/cyngn/eleven/adapters/AlbumArtPagerAdapter.java | 301 | ||||
-rw-r--r-- | src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java | 101 | ||||
-rw-r--r-- | src/com/cyngn/eleven/utils/MusicUtils.java | 88 | ||||
-rw-r--r-- | src/com/cyngn/eleven/widgets/RepeatingImageButton.java | 2 | ||||
-rw-r--r-- | src/com/cyngn/eleven/widgets/SquareViewPager.java | 33 |
9 files changed, 768 insertions, 94 deletions
diff --git a/res/layout/activity_player_fragment.xml b/res/layout/activity_player_fragment.xml index a273c06..5b7affa 100644 --- a/res/layout/activity_player_fragment.xml +++ b/res/layout/activity_player_fragment.xml @@ -32,24 +32,22 @@ android:layout_alignParentRight="true" android:layout_alignParentTop="true" > - <com.cyngn.eleven.widgets.SquareImageView - android:id="@+id/audio_player_album_art" + <com.cyngn.eleven.widgets.SquareViewPager + android:id="@+id/audio_player_album_art_viewpager" android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_centerHorizontal="true" - android:scaleType="fitXY" /> + android:layout_height="match_parent" /> <ImageView android:layout_width="match_parent" android:layout_height="@dimen/shadow_height" - android:layout_alignTop="@+id/audio_player_album_art" + android:layout_alignTop="@+id/audio_player_album_art_viewpager" android:contentDescription="@null" android:src="@drawable/top_shadow" /> <ImageView android:layout_width="match_parent" android:layout_height="@dimen/shadow_height" - android:layout_alignBottom="@+id/audio_player_album_art" + android:layout_alignBottom="@+id/audio_player_album_art_viewpager" android:contentDescription="@null" android:src="@drawable/bottom_shadow" /> diff --git a/res/layout/album_art_fragment.xml b/res/layout/album_art_fragment.xml new file mode 100644 index 0000000..f8b7cca --- /dev/null +++ b/res/layout/album_art_fragment.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 Cyanogen, Inc. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <com.cyngn.eleven.widgets.SquareImageView + android:id="@+id/audio_player_album_art" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_centerHorizontal="true" + android:scaleType="fitXY" + android:background="@drawable/default_artwork"/> +</FrameLayout>
\ No newline at end of file diff --git a/src/com/cyngn/eleven/IElevenService.aidl b/src/com/cyngn/eleven/IElevenService.aidl index 04347e5..97a7bee 100644 --- a/src/com/cyngn/eleven/IElevenService.aidl +++ b/src/com/cyngn/eleven/IElevenService.aidl @@ -9,7 +9,7 @@ interface IElevenService void stop(); void pause(); void play(); - void prev(); + void prev(boolean forcePrevious); void next(); void enqueue(in long [] list, int action); void setQueuePosition(int index); @@ -19,17 +19,21 @@ interface IElevenService void refresh(); boolean isPlaying(); long [] getQueue(); + int getQueuePosition(); + int getQueueHistorySize(); + int[] getQueueHistoryList(); long duration(); long position(); long seek(long pos); long getAudioId(); + long getNextAudioId(); + long getPreviousAudioId(); long getArtistId(); long getAlbumId(); String getArtistName(); String getTrackName(); String getAlbumName(); String getPath(); - int getQueuePosition(); int getShuffleMode(); int removeTracks(int first, int last); int removeTrack(long id); diff --git a/src/com/cyngn/eleven/MusicPlaybackService.java b/src/com/cyngn/eleven/MusicPlaybackService.java index 23df4d5..d869fd4 100644 --- a/src/com/cyngn/eleven/MusicPlaybackService.java +++ b/src/com/cyngn/eleven/MusicPlaybackService.java @@ -134,11 +134,16 @@ public class MusicPlaybackService extends Service { public static final String STOP_ACTION = "com.cyngn.eleven.stop"; /** - * Called to go to the previous track + * Called to go to the previous track or the beginning of the track if partway through the track */ public static final String PREVIOUS_ACTION = "com.cyngn.eleven.previous"; /** + * Called to go to the previous track regardless of how far in the current track the playback is + */ + public static final String PREVIOUS_FORCE_ACTION = "com.cyngn.eleven.previous.force"; + + /** * Called to go to the next track */ public static final String NEXT_ACTION = "com.cyngn.eleven.next"; @@ -290,8 +295,9 @@ public class MusicPlaybackService extends Service { /** * The max size allowed for the track history + * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo */ - private static final int MAX_HISTORY_SIZE = 100; + public static final int MAX_HISTORY_SIZE = 1000; /** * The columns used to retrieve any info from the current track @@ -582,6 +588,7 @@ public class MusicPlaybackService extends Service { filter.addAction(STOP_ACTION); filter.addAction(NEXT_ACTION); filter.addAction(PREVIOUS_ACTION); + filter.addAction(PREVIOUS_FORCE_ACTION); filter.addAction(REPEAT_ACTION); filter.addAction(SHUFFLE_ACTION); // Attach the broadcast listener @@ -752,13 +759,9 @@ public class MusicPlaybackService extends Service { if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) { gotoNext(true); - } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) { - if (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD) { - prev(); - } else { - seek(0); - play(); - } + } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action) + || PREVIOUS_FORCE_ACTION.equals(action)) { + prev(PREVIOUS_FORCE_ACTION.equals(action)); } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) { if (isPlaying()) { pause(); @@ -1111,61 +1114,80 @@ public class MusicPlaybackService extends Service { /** * @param force True to force the player onto the track next, false * otherwise. + * @param saveToHistory True to save the mPlayPos to the history * @return The next position to play. */ private int getNextPosition(final boolean force) { + // if we're not forced to go to the next track and we are only playing the current track if (!force && mRepeatMode == REPEAT_CURRENT) { if (mPlayPos < 0) { return 0; } return mPlayPos; } else if (mShuffleMode == SHUFFLE_NORMAL) { - if (mPlayPos >= 0) { - mHistory.add(mPlayPos); - } - if (mHistory.size() > MAX_HISTORY_SIZE) { - mHistory.remove(0); - } final int numTracks = mPlayListLen; - final int[] tracks = new int[numTracks]; + + // count the number of times a track has been played + final int[] trackNumPlays = new int[numTracks]; for (int i = 0; i < numTracks; i++) { - tracks[i] = i; + // set it all to 0 + trackNumPlays[i] = 0; } + // walk through the history and add up the number of times the track + // has been played final int numHistory = mHistory.size(); - int numUnplayed = numTracks; for (int i = 0; i < numHistory; i++) { final int idx = mHistory.get(i).intValue(); - if (idx < numTracks && tracks[idx] >= 0) { - numUnplayed--; - tracks[idx] = -1; + if (idx >= 0 && idx < numTracks) { + trackNumPlays[idx]++; } } - if (numUnplayed <= 0) { - if (mRepeatMode == REPEAT_ALL || force) { - numUnplayed = numTracks; - for (int i = 0; i < numTracks; i++) { - tracks[i] = i; - } - } else { - return -1; - } + + // also add the currently playing track to the count + if (mPlayPos >= 0 && mPlayPos < numTracks) { + trackNumPlays[mPlayPos]++; } - int skip = 0; - if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) { - skip = mShuffler.nextInt(numUnplayed); + + // figure out the least # of times a track has a played as well as + // how many tracks share that count + int minNumPlays = Integer.MAX_VALUE; + int numTracksWithMinNumPlays = 0; + for (int i = 0; i < trackNumPlays.length; i++) { + // if we found a new track that has less number of plays, reset the counters + if (trackNumPlays[i] < minNumPlays) { + minNumPlays = trackNumPlays[i]; + numTracksWithMinNumPlays = 1; + } else if (trackNumPlays[i] == minNumPlays) { + // increment this track shares the # of tracks + numTracksWithMinNumPlays++; + } } - int cnt = -1; - while (true) { - while (tracks[++cnt] < 0) { - ; + + // if we've played each track at least once + if (minNumPlays > 0) { + // if we aren't repeating all and we're not forcing a track + // return no more tracks + if (mRepeatMode != REPEAT_ALL && !force) { + return -1; } - skip--; - if (skip < 0) { - break; + } + + // else pick a track from the least number of played tracks + int skip = mShuffler.nextInt(numTracksWithMinNumPlays); + for (int i = 0; i < trackNumPlays.length; i++) { + if (trackNumPlays[i] == minNumPlays) { + if (skip == 0) { + return i; + } else { + skip--; + } } } - return cnt; + + // Unexpected to land here + if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have"); + return -1; } else if (mShuffleMode == SHUFFLE_AUTO) { doAutoShuffleUpdate(); return mPlayPos + 1; @@ -1184,10 +1206,18 @@ public class MusicPlaybackService extends Service { } /** - * Sets the track track to be played + * Sets the track to be played */ private void setNextTrack() { - mNextPlayPos = getNextPosition(false); + setNextTrack(getNextPosition(false)); + } + + /** + * Sets the next track to be played + * @param position the target position we want + */ + private void setNextTrack(int position) { + mNextPlayPos = position; if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos); if (mNextPlayPos >= 0 && mPlayList != null) { final long id = mPlayList[mNextPlayPos]; @@ -1614,6 +1644,7 @@ public class MusicPlaybackService extends Service { mPlayListLen = 1; mPlayList[0] = mCursor.getLong(IDCOLIDX); mPlayPos = 0; + mHistory.clear(); } } catch (final UnsupportedOperationException ex) { } @@ -1718,6 +1749,29 @@ public class MusicPlaybackService extends Service { } /** + * @return the size of the queue history cache + */ + public int getQueueHistorySize() { + synchronized (this) { + return mHistory.size(); + } + } + + /** + * @return the queue of history positions + */ + public int[] getQueueHistoryList() { + synchronized (this) { + int[] history = new int[mHistory.size()]; + for (int i = 0; i < mHistory.size(); i++) { + history[i] = mHistory.get(i); + } + + return history; + } + } + + /** * Returns the path to current song * * @return The path to the current song @@ -1830,6 +1884,37 @@ public class MusicPlaybackService extends Service { } /** + * Returns the next audio ID + * + * @return The next track ID + */ + public long getNextAudioId() { + synchronized (this) { + if (mNextPlayPos >= 0 && mPlayer.isInitialized()) { + return mPlayList[mNextPlayPos]; + } + } + return -1; + } + + /** + * Returns the previous audio ID + * + * @return The previous track ID + */ + public long getPreviousAudioId() { + synchronized (this) { + if (mPlayer.isInitialized()) { + int pos = getPreviousPlayPosition(false); + if (pos >= 0) { + return mPlayList[pos]; + } + } + } + return -1; + } + + /** * Seeks the current track to a specific time * * @param position The time to seek to @@ -1947,6 +2032,15 @@ public class MusicPlaybackService extends Service { * Resumes or starts playback. */ public void play() { + play(true); + } + + /** + * Resumes or starts playback. + * @param createNewNextTrack True if you want to figure out the next track, false + * if you want to re-use the existing next track (used for going back) + */ + public void play(boolean createNewNextTrack) { int status = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); @@ -1959,7 +2053,11 @@ public class MusicPlaybackService extends Service { mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName())); - setNextTrack(); + if (createNewNextTrack) { + setNextTrack(); + } else { + setNextTrack(mNextPlayPos); + } if (mPlayer.isInitialized()) { final long duration = mPlayer.duration(); @@ -2011,7 +2109,11 @@ public class MusicPlaybackService extends Service { scheduleDelayedShutdown(); return; } - final int pos = getNextPosition(force); + int pos = mNextPlayPos; + if (pos < 0) { + pos = getNextPosition(force); + } + if (pos < 0) { scheduleDelayedShutdown(); if (mIsSupposedToBePlaying) { @@ -2020,40 +2122,80 @@ public class MusicPlaybackService extends Service { } return; } - mPlayPos = pos; + stop(false); - mPlayPos = pos; + setAndRecordPlayPos(pos); openCurrentAndNext(); play(); notifyChange(META_CHANGED); } } + public void setAndRecordPlayPos(int nextPos) { + synchronized (this) { + // save to the history + if (mShuffleMode != SHUFFLE_NONE) { + mHistory.add(mPlayPos); + if (mHistory.size() > MAX_HISTORY_SIZE) { + mHistory.remove(0); + } + } + + mPlayPos = nextPos; + } + } + /** * Changes from the current track to the previous played track */ - public void prev() { - if (D) Log.d(TAG, "Going to previous track"); + public void prev(boolean forcePrevious) { + synchronized (this) { + // if we aren't repeating 1, and we are either early in the song + // or we want to force go back, then go to the prevous track + boolean goPrevious = getRepeatMode() != REPEAT_CURRENT && + (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious); + + if (goPrevious) { + if (D) Log.d(TAG, "Going to previous track"); + int pos = getPreviousPlayPosition(true); + // if we have no more previous tracks, quit + if (pos < 0) { + return; + } + mNextPlayPos = mPlayPos; + mPlayPos = pos; + stop(false); + openCurrent(); + play(false); + notifyChange(META_CHANGED); + } else { + if (D) Log.d(TAG, "Going to beginning of track"); + seek(0); + play(false); + } + } + } + + public int getPreviousPlayPosition(boolean removeFromHistory) { synchronized (this) { if (mShuffleMode == SHUFFLE_NORMAL) { // Go to previously-played track and remove it from the history final int histsize = mHistory.size(); if (histsize == 0) { - return; + return -1; } - final Integer pos = mHistory.remove(histsize - 1); - mPlayPos = pos.intValue(); + final Integer pos = mHistory.get(histsize - 1); + if (removeFromHistory) { + mHistory.remove(histsize - 1); + } + return pos.intValue(); } else { if (mPlayPos > 0) { - mPlayPos--; + return mPlayPos - 1; } else { - mPlayPos = mPlayListLen - 1; + return mPlayListLen - 1; } } - stop(false); - openCurrent(); - play(); - notifyChange(META_CHANGED); } } @@ -2131,6 +2273,7 @@ public class MusicPlaybackService extends Service { if (mShuffleMode == shufflemode && mPlayListLen > 0) { return; } + mShuffleMode = shufflemode; if (mShuffleMode == SHUFFLE_AUTO) { if (makeAutoShuffleList()) { @@ -2144,6 +2287,8 @@ public class MusicPlaybackService extends Service { } else { mShuffleMode = SHUFFLE_NONE; } + } else { + setNextTrack(); } saveQueue(false); notifyChange(SHUFFLEMODE_CHANGED); @@ -2335,14 +2480,14 @@ public class MusicPlaybackService extends Service { } break; case TRACK_WENT_TO_NEXT: - service.mPlayPos = service.mNextPlayPos; + service.setAndRecordPlayPos(service.mNextPlayPos); + service.setNextTrack(); if (service.mCursor != null) { service.mCursor.close(); } service.updateCursor(service.mPlayList[service.mPlayPos]); service.notifyChange(META_CHANGED); service.updateNotification(); - service.setNextTrack(); break; case TRACK_ENDED: if (service.mRepeatMode == REPEAT_CURRENT) { @@ -2730,8 +2875,8 @@ public class MusicPlaybackService extends Service { * {@inheritDoc} */ @Override - public void prev() throws RemoteException { - mService.get().prev(); + public void prev(boolean forcePrevious) throws RemoteException { + mService.get().prev(forcePrevious); } /** @@ -2810,6 +2955,22 @@ public class MusicPlaybackService extends Service { * {@inheritDoc} */ @Override + public int getQueueHistorySize() throws RemoteException { + return mService.get().getQueueHistorySize(); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] getQueueHistoryList() throws RemoteException { + return mService.get().getQueueHistoryList(); + } + + /** + * {@inheritDoc} + */ + @Override public long duration() throws RemoteException { return mService.get().duration(); } @@ -2842,6 +3003,22 @@ public class MusicPlaybackService extends Service { * {@inheritDoc} */ @Override + public long getNextAudioId() throws RemoteException { + return mService.get().getNextAudioId(); + } + + /** + * {@inheritDoc} + */ + @Override + public long getPreviousAudioId() throws RemoteException { + return mService.get().getPreviousAudioId(); + } + + /** + * {@inheritDoc} + */ + @Override public long getArtistId() throws RemoteException { return mService.get().getArtistId(); } diff --git a/src/com/cyngn/eleven/adapters/AlbumArtPagerAdapter.java b/src/com/cyngn/eleven/adapters/AlbumArtPagerAdapter.java new file mode 100644 index 0000000..2a03ea6 --- /dev/null +++ b/src/com/cyngn/eleven/adapters/AlbumArtPagerAdapter.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2014 Cyanogen, Inc. + */ + +package com.cyngn.eleven.adapters; + +import android.content.Context; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.provider.MediaStore; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.cyngn.eleven.MusicPlaybackService; +import com.cyngn.eleven.R; +import com.cyngn.eleven.utils.ApolloUtils; +import com.cyngn.eleven.utils.MusicUtils; +import com.cyngn.eleven.widgets.SquareImageView; + +import java.util.Iterator; +import java.util.LinkedList; + +/** + * A {@link android.support.v4.app.FragmentStatePagerAdapter} class for swiping between album art + */ +public class AlbumArtPagerAdapter extends FragmentStatePagerAdapter { + private static boolean DEBUG = false; + private static final String TAG = AlbumArtPagerAdapter.class.getSimpleName(); + + public static final long NO_TRACK_ID = -1; + private static final int MAX_ALBUM_ARTIST_SIZE = 10; + + // This helps with flickering and jumping and reloading the same tracks + private final static LinkedList<AlbumArtistDetails> sCacheAlbumArtistDetails = new LinkedList<AlbumArtistDetails>(); + + /** + * Adds the album artist details to the cache + * @param details the AlbumArtistDetails to add + */ + public static void addAlbumArtistDetails(AlbumArtistDetails details) { + if (getAlbumArtistDetails(details.mAudioId) == null) { + sCacheAlbumArtistDetails.add(details); + if (sCacheAlbumArtistDetails.size() > MAX_ALBUM_ARTIST_SIZE) { + sCacheAlbumArtistDetails.remove(); + } + } + } + + /** + * Gets the album artist details for the audio track. If it exists, it re-inserts the item + * to the end of the queue so it is considered the 'freshest' and stays longer + * @param audioId the audio track to look for + * @return the details of the album artist + */ + public static AlbumArtistDetails getAlbumArtistDetails(long audioId) { + for (Iterator<AlbumArtistDetails> i = sCacheAlbumArtistDetails.descendingIterator(); i.hasNext();) { + final AlbumArtistDetails entry = i.next(); + if (entry.mAudioId == audioId) { + // remove it from the stack to re-add to the top + i.remove(); + sCacheAlbumArtistDetails.add(entry); + return entry; + } + } + + return null; + } + + // the length of the playlist + private int mPlaylistLen = 0; + + public AlbumArtPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(final int position) { + long trackID = getTrackId(position); + return AlbumArtFragment.newInstance(trackID); + } + + @Override + public int getCount() { + return mPlaylistLen; + } + + public void setPlaylistLength(final int len) { + mPlaylistLen = len; + notifyDataSetChanged(); + } + + /** + * Gets the track id for the item at position + * @param position position of the item of the queue + * @return track id of the item at position or NO_TRACK_ID if unknown + */ + private long getTrackId(int position) { + if (MusicUtils.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) { + // if we are only playing one song, return the current audio id + return MusicUtils.getCurrentAudioId(); + } else if (MusicUtils.getShuffleMode() == MusicPlaybackService.SHUFFLE_NONE) { + // if we aren't shuffling, just return based on the queue position + return MusicUtils.getQueue()[position]; + } else { + // if we are shuffling, there is no 'queue' going forward per say + // because it is dynamically generated. In that case we can only look + // at the history and up to the very next track. When we come back to this + // after the demo, we should redo that queue logic to be able to give us + // tracks going forward + + // how far into the history we are + int positionOffset = MusicUtils.getQueueHistorySize(); + + if (position - positionOffset == 0) { // current track + return MusicUtils.getCurrentAudioId(); + } else if (position - positionOffset == 1) { // next track + return MusicUtils.getNextAudioId(); + } else if (position < positionOffset) { + // historical track - look up the historical position + long[] audioIds = MusicUtils.getQueue(); + int[] historyPositions = MusicUtils.getQueueHistoryList(); + if (position < historyPositions.length) { + int queuePosition = historyPositions[position]; + if (queuePosition < audioIds.length) { + return audioIds[queuePosition]; + } + } + } + } + + // fallback case + return NO_TRACK_ID; + } + + /** + * The fragments to be displayed inside this adapter. This wraps the album art + * and handles loading the album art for a given audio id + */ + public static class AlbumArtFragment extends Fragment { + private static final String ID = "com.cyngn.eleven.adapters.AlbumArtPagerAdapter.AlbumArtFragment.ID"; + + private View mRootView; + private AlbumArtistLoader mTask; + private SquareImageView mImageView; + private long mAudioId = NO_TRACK_ID; + + public static AlbumArtFragment newInstance(final long trackId) { + AlbumArtFragment frag = new AlbumArtFragment(); + final Bundle args = new Bundle(); + args.putLong(ID, trackId); + frag.setArguments(args); + return frag; + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAudioId = getArguments().getLong(ID, NO_TRACK_ID); + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.album_art_fragment, null); + return mRootView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + // if we are destroying our view, cancel our task and null it + if (mTask != null) { + mTask.cancel(true); + mTask = null; + } + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mImageView = (SquareImageView)mRootView.findViewById(R.id.audio_player_album_art); + loadImageAsync(); + } + + /** + * Loads the image asynchronously + */ + private void loadImageAsync() { + // if we have no track id, quit + if (mAudioId == NO_TRACK_ID) { + return; + } + + // try loading from the cache + AlbumArtistDetails details = getAlbumArtistDetails(mAudioId); + if (details != null) { + loadImageAsync(details); + } else { + mTask = new AlbumArtistLoader(this, getActivity()); + ApolloUtils.execute(false, mTask, mAudioId); + } + + } + + /** + * Loads the image asynchronously + * @param details details of the image to load + */ + private void loadImageAsync(AlbumArtistDetails details) { + // load the actual image + ApolloUtils.getImageFetcher(getActivity()).loadAlbumImage( + details.mArtistName, + details.mAlbumName, + details.mAlbumId, + mImageView + ); + } + } + + /** + * Simple container for the album and artist name as well as album id + */ + private static class AlbumArtistDetails { + public long mAudioId; + public long mAlbumId; + public String mAlbumName; + public String mArtistName; + } + + /** + * This looks up the album and artist details for a track + */ + private static class AlbumArtistLoader extends AsyncTask<Long, Void, AlbumArtistDetails> { + private Context mContext; + private AlbumArtFragment mFragment; + + public AlbumArtistLoader(final AlbumArtFragment albumArtFragment, final Context context) { + mContext = context; + mFragment = albumArtFragment; + } + + @Override + protected AlbumArtistDetails doInBackground(final Long... params) { + long id = params[0]; + + final StringBuilder selection = new StringBuilder(); + selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"); + selection.append(" AND " + BaseColumns._ID + " = '" + id + "'"); + + Cursor cursor = mContext.getContentResolver().query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + new String[] { + /* 0 */ + MediaStore.Audio.AudioColumns.ALBUM_ID, + /* 1 */ + MediaStore.Audio.AudioColumns.ALBUM, + /* 2 */ + MediaStore.Audio.AlbumColumns.ARTIST, + }, selection.toString(), null, null); + + if (!cursor.moveToFirst()) { + cursor.close(); + return null; + } + + AlbumArtistDetails result = new AlbumArtistDetails(); + result.mAudioId = id; + result.mAlbumId = cursor.getLong(0); + result.mAlbumName = cursor.getString(1); + result.mArtistName = cursor.getString(2); + cursor.close(); + + return result; + } + + @Override + protected void onPostExecute(final AlbumArtistDetails result) { + if (result != null) { + if (DEBUG) { + Log.d(TAG, "[" + mFragment.mAudioId + "] Loading image: " + + result.mAlbumId + "," + + result.mAlbumName + "," + + result.mArtistName); + } + + AlbumArtPagerAdapter.addAlbumArtistDetails(result); + mFragment.loadImageAsync(result); + } else if (DEBUG) { + Log.d(TAG, "No Image found for audioId: " + mFragment.mAudioId); + } + } + } +} diff --git a/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java b/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java index ef5925a..f6876ce 100644 --- a/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java +++ b/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java @@ -3,7 +3,6 @@ */ package com.cyngn.eleven.ui.fragments; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -18,13 +17,14 @@ import android.os.IBinder; import android.os.Message; import android.os.SystemClock; import android.support.v4.app.Fragment; +import android.support.v4.view.ViewPager; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; @@ -34,6 +34,7 @@ import android.provider.MediaStore.Audio.Albums; import android.provider.MediaStore.Audio.Artists; import com.cyngn.eleven.MusicPlaybackService; import com.cyngn.eleven.R; +import com.cyngn.eleven.adapters.AlbumArtPagerAdapter; import com.cyngn.eleven.cache.ImageFetcher; import com.cyngn.eleven.menu.DeleteDialog; import com.cyngn.eleven.ui.HeaderBar; @@ -52,6 +53,7 @@ import static com.cyngn.eleven.utils.MusicUtils.mService; public class AudioPlayerFragment extends Fragment implements ServiceConnection, SeekBar.OnSeekBarChangeListener, HeaderBar.PopupMenuCreator { + private static final String TAG = AudioPlayerFragment.class.getSimpleName(); // fragment view private ViewGroup mRootView; @@ -83,8 +85,10 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, // Artist name private TextView mArtistName; - // Album art - private ImageView mAlbumArt; + // Album art ListView + private ViewPager mAlbumArtViewPager; + + private AlbumArtPagerAdapter mAlbumArtPagerAdapter; // Current time private TextView mCurrentTime; @@ -163,6 +167,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, startPlayback(); // Set the playback drawables updatePlaybackControls(); + // Setup the adapter + createAndSetAdapter(); // Current info updateNowPlayingInfo(); } @@ -249,6 +255,9 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, filter.addAction(MusicPlaybackService.META_CHANGED); // Update a list, probably the playlist fragment's filter.addAction(MusicPlaybackService.REFRESH); + // Listen to changes to the entire queue + filter.addAction(MusicPlaybackService.QUEUE_CHANGED); + // Register the intent filters getActivity().registerReceiver(mPlaybackStatus, filter); // Refresh the current time final long next = refreshCurrentTime(); @@ -306,8 +315,37 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, mTrackName = (TextView)mRootView.findViewById(R.id.audio_player_track_name); // Artist name mArtistName = (TextView)mRootView.findViewById(R.id.audio_player_artist_name); - // Album art - mAlbumArt = (ImageView)mRootView.findViewById(R.id.audio_player_album_art); + + + + // Album art view pager + mAlbumArtViewPager = (ViewPager)mRootView.findViewById(R.id.audio_player_album_art_viewpager); + mAlbumArtViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + + int currentPosition = 0; + if (MusicUtils.getShuffleMode() == MusicPlaybackService.SHUFFLE_NONE) { + // if we aren't shuffling, base the position on the queue position + currentPosition = MusicUtils.getQueuePosition(); + } else { + // if we are shuffling, use the history size as the position + currentPosition = MusicUtils.getQueueHistorySize(); + } + + // check if we are going to next or previous track + if (position - currentPosition == 1) { + MusicUtils.asyncNext(getActivity()); + } else if (position - currentPosition == -1) { + MusicUtils.previous(getActivity(), true); + } else if (currentPosition != position) { + Log.w(TAG, "Unexpected page position of " + position + + " when current is: " + currentPosition); + } + } + }); + // Current time mCurrentTime = (TextView)mRootView.findViewById(R.id.audio_player_current_time); // Total time @@ -333,11 +371,49 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, mArtistName.setText(MusicUtils.getArtistName()); // Set the total time mTotalTime.setText(MusicUtils.makeTimeString(getActivity(), MusicUtils.duration() / 1000)); - // Set the album art - mImageFetcher.loadCurrentArtwork(mAlbumArt); + + if (MusicUtils.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) { + // we are repeating 1 so just jump to the 1st and only item + mAlbumArtViewPager.setCurrentItem(0, false); + } else if (MusicUtils.getShuffleMode() == MusicPlaybackService.SHUFFLE_NONE) { + // we are playing in-order, base the position on the queue position + mAlbumArtViewPager.setCurrentItem(MusicUtils.getQueuePosition(), true); + } else { + // if we are shuffling, just based our index based on the history + mAlbumArtViewPager.setCurrentItem(MusicUtils.getQueueHistorySize(), true); + } + // Update the current time queueNextRefresh(1); + } + /** + * This creates the adapter based on the repeat and shuffle configuration and sets it into the + * page adapter + */ + private void createAndSetAdapter() { + mAlbumArtPagerAdapter = new AlbumArtPagerAdapter(getFragmentManager()); + + int repeatMode = MusicUtils.getRepeatMode(); + int targetSize = 0; + int targetIndex = 0; + + if (repeatMode == MusicPlaybackService.REPEAT_CURRENT) { + targetSize = 1; + targetIndex = 0; + } else if (MusicUtils.getShuffleMode() == MusicPlaybackService.SHUFFLE_NONE) { + // if we aren't shuffling, use the queue to determine where we are + targetSize = MusicUtils.getQueue().length; + targetIndex = MusicUtils.getQueuePosition(); + } else { + // otherwise, set it to the max history size + targetSize = MusicPlaybackService.MAX_HISTORY_SIZE; + targetIndex = MusicUtils.getQueueHistorySize(); + } + + mAlbumArtPagerAdapter.setPlaylistLength(targetSize); + mAlbumArtViewPager.setAdapter(mAlbumArtPagerAdapter); + mAlbumArtViewPager.setCurrentItem(targetIndex); } private long parseIdFromIntent(Intent intent, String longKey, @@ -400,8 +476,6 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, if (handled) { // Make sure to process intent only once getActivity().setIntent(new Intent()); - // Refresh the queue - // TODO: Refresh queue or have it self-aware return true; } @@ -455,7 +529,7 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, long newpos = mStartSeekPos - delta; if (newpos < 0) { // move to previous track - MusicUtils.previous(getActivity()); + MusicUtils.previous(getActivity(), true); final long duration = MusicUtils.duration(); mStartSeekPos += duration; newpos += duration; @@ -750,6 +824,11 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection, mReference.get().mRepeatButton.updateRepeatState(); // Set the shuffle image mReference.get().mShuffleButton.updateShuffleState(); + + // Update the queue + mReference.get().createAndSetAdapter(); + } else if (action.equals(MusicPlaybackService.QUEUE_CHANGED)) { + mReference.get().createAndSetAdapter(); } } } diff --git a/src/com/cyngn/eleven/utils/MusicUtils.java b/src/com/cyngn/eleven/utils/MusicUtils.java index e5977c0..4e963d7 100644 --- a/src/com/cyngn/eleven/utils/MusicUtils.java +++ b/src/com/cyngn/eleven/utils/MusicUtils.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.database.Cursor; import android.net.Uri; +import android.os.AsyncTask; import android.os.IBinder; import android.os.RemoteException; import android.provider.BaseColumns; @@ -211,6 +212,15 @@ public final class MusicUtils { } /** + * Changes to the next track asynchronously + */ + public static void asyncNext(final Context context) { + final Intent previous = new Intent(context, MusicPlaybackService.class); + previous.setAction(MusicPlaybackService.NEXT_ACTION); + context.startService(previous); + } + + /** * Changes to the previous track. * * @NOTE The AIDL isn't used here in order to properly use the previous @@ -224,9 +234,13 @@ public final class MusicUtils { * less than 2000 ms, start the track over, otherwise move to the * previously listened track. */ - public static void previous(final Context context) { + public static void previous(final Context context, final boolean force) { final Intent previous = new Intent(context, MusicPlaybackService.class); - previous.setAction(MusicPlaybackService.PREVIOUS_ACTION); + if (force) { + previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION); + } else { + previous.setAction(MusicPlaybackService.PREVIOUS_ACTION); + } context.startService(previous); } @@ -403,6 +417,32 @@ public final class MusicUtils { } /** + * @return The next song Id. + */ + public static final long getNextAudioId() { + if (mService != null) { + try { + return mService.getNextAudioId(); + } catch (final RemoteException ignored) { + } + } + return -1; + } + + /** + * @return The previous song Id. + */ + public static final long getPreviousAudioId() { + if (mService != null) { + try { + return mService.getPreviousAudioId(); + } catch (final RemoteException ignored) { + } + } + return -1; + } + + /** * @return The current artist Id. */ public static final long getCurrentArtistId() { @@ -443,28 +483,54 @@ public final class MusicUtils { } /** - * @param id The ID of the track to remove. - * @return removes track from a playlist or the queue. + * @return The position of the current track in the queue. */ - public static final int removeTrack(final long id) { + public static final int getQueuePosition() { try { if (mService != null) { - return mService.removeTrack(id); + return mService.getQueuePosition(); } - } catch (final RemoteException ingored) { + } catch (final RemoteException ignored) { } return 0; } /** - * @return The position of the current track in the queue. + * @return The queue history size */ - public static final int getQueuePosition() { + public static final int getQueueHistorySize() { + if (mService != null) { + try { + return mService.getQueueHistorySize(); + } catch (final RemoteException ignored) { + } + } + return 0; + } + + /** + * @return The queue history + */ + public static final int[] getQueueHistoryList() { + if (mService != null) { + try { + return mService.getQueueHistoryList(); + } catch (final RemoteException ignored) { + } + } + return null; + } + + /** + * @param id The ID of the track to remove. + * @return removes track from a playlist or the queue. + */ + public static final int removeTrack(final long id) { try { if (mService != null) { - return mService.getQueuePosition(); + return mService.removeTrack(id); } - } catch (final RemoteException ignored) { + } catch (final RemoteException ingored) { } return 0; } diff --git a/src/com/cyngn/eleven/widgets/RepeatingImageButton.java b/src/com/cyngn/eleven/widgets/RepeatingImageButton.java index 6aaabcd..0becdb4 100644 --- a/src/com/cyngn/eleven/widgets/RepeatingImageButton.java +++ b/src/com/cyngn/eleven/widgets/RepeatingImageButton.java @@ -72,7 +72,7 @@ public class RepeatingImageButton extends ImageButton implements OnClickListener public void onClick(final View view) { switch (view.getId()) { case R.id.action_button_previous: - MusicUtils.previous(getContext()); + MusicUtils.previous(getContext(), false); break; case R.id.action_button_next: MusicUtils.next(); diff --git a/src/com/cyngn/eleven/widgets/SquareViewPager.java b/src/com/cyngn/eleven/widgets/SquareViewPager.java new file mode 100644 index 0000000..da1d5f5 --- /dev/null +++ b/src/com/cyngn/eleven/widgets/SquareViewPager.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 Cyanogen, Inc. + */ +package com.cyngn.eleven.widgets; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; + +/** + * A custom {@link ViewPager} that is sized to be a perfect square, otherwise + * functions like a typical {@link ViewPager}. + */ +public class SquareViewPager extends ViewPager { + + /** + * @param context The {@link Context} to use + * @param attrs The attributes of the XML tag that is inflating the view. + */ + public SquareViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int mSize = Math.min(getMeasuredWidth(), getMeasuredHeight()); + setMeasuredDimension(mSize, mSize); + } +} |