summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/layout/activity_player_fragment.xml12
-rw-r--r--res/layout/album_art_fragment.xml16
-rw-r--r--src/com/cyngn/eleven/IElevenService.aidl8
-rw-r--r--src/com/cyngn/eleven/MusicPlaybackService.java301
-rw-r--r--src/com/cyngn/eleven/adapters/AlbumArtPagerAdapter.java301
-rw-r--r--src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java101
-rw-r--r--src/com/cyngn/eleven/utils/MusicUtils.java88
-rw-r--r--src/com/cyngn/eleven/widgets/RepeatingImageButton.java2
-rw-r--r--src/com/cyngn/eleven/widgets/SquareViewPager.java33
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);
+ }
+}