summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
authorlinus_lee <llee@cyngn.com>2014-11-19 12:47:04 -0800
committerlinus_lee <llee@cyngn.com>2014-12-09 11:59:39 -0800
commitad606c2e575a45174ac81f18ee34985616458d0c (patch)
tree6042a94864a3ce283894a46f85331aae8830f443 /src/com
parent1f86c5725d8ca216040199be2a1c664d1861eed8 (diff)
downloadandroid_packages_apps_Eleven-ad606c2e575a45174ac81f18ee34985616458d0c.tar.gz
android_packages_apps_Eleven-ad606c2e575a45174ac81f18ee34985616458d0c.tar.bz2
android_packages_apps_Eleven-ad606c2e575a45174ac81f18ee34985616458d0c.zip
Eleven: Add Lyric (srt) support
https://cyanogen.atlassian.net/browse/MUSIC-186 Change-Id: I8fec1c61d69dca06be30ccebf9c996d7fac2ae75
Diffstat (limited to 'src/com')
-rw-r--r--src/com/cyngn/eleven/MusicPlaybackService.java82
-rw-r--r--src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java40
-rw-r--r--src/com/cyngn/eleven/utils/PreferenceUtils.java10
-rw-r--r--src/com/cyngn/eleven/utils/SrtManager.java199
-rw-r--r--src/com/cyngn/eleven/utils/SrtParser.java119
5 files changed, 443 insertions, 7 deletions
diff --git a/src/com/cyngn/eleven/MusicPlaybackService.java b/src/com/cyngn/eleven/MusicPlaybackService.java
index 8e514fa..674a3a4 100644
--- a/src/com/cyngn/eleven/MusicPlaybackService.java
+++ b/src/com/cyngn/eleven/MusicPlaybackService.java
@@ -61,7 +61,9 @@ import com.cyngn.eleven.provider.SongPlayCount;
import com.cyngn.eleven.service.MusicPlaybackTrack;
import com.cyngn.eleven.utils.ApolloUtils;
import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.SrtManager;
+import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -189,6 +191,11 @@ public class MusicPlaybackService extends Service {
private static final String SHUTDOWN = "com.cyngn.eleven.shutdown";
/**
+ * Called to notify of a timed text
+ */
+ public static final String NEW_LYRICS = "com.cyngn.eleven.lyrics";
+
+ /**
* Called to update the remote control client
*/
public static final String UPDATE_LOCKSCREEN = "com.cyngn.eleven.updatelockscreen";
@@ -287,6 +294,11 @@ public class MusicPlaybackService extends Service {
private static final int FADEUP = 7;
/**
+ * Notifies that there is a new timed text string
+ */
+ private static final int LYRICS = 8;
+
+ /**
* Idle time before stopping the foreground notfication (5 minutes)
*/
private static final int IDLE_DELAY = 5 * 60 * 1000;
@@ -450,6 +462,8 @@ public class MusicPlaybackService extends Service {
private int mServiceStartId = -1;
+ private String mLyrics;
+
private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<MusicPlaybackTrack>(100);
private long[] mAutoShuffleList = null;
@@ -1373,6 +1387,11 @@ public class MusicPlaybackService extends Service {
intent.putExtra("album", getAlbumName());
intent.putExtra("track", getTrackName());
intent.putExtra("playing", isPlaying());
+
+ if (NEW_LYRICS.equals(what)) {
+ intent.putExtra("lyrics", mLyrics);
+ }
+
sendStickyBroadcast(intent);
final Intent musicIntent = new Intent(intent);
@@ -2682,6 +2701,10 @@ public class MusicPlaybackService extends Service {
service.gotoNext(false);
}
break;
+ case LYRICS:
+ service.mLyrics = (String) msg.obj;
+ service.notifyChange(NEW_LYRICS);
+ break;
case RELEASE_WAKELOCK:
service.mWakeLock.release();
break;
@@ -2781,12 +2804,22 @@ public class MusicPlaybackService extends Service {
private boolean mIsInitialized = false;
+ private SrtManager mSrtManager;
+
+ private String mNextMediaPath;
+
/**
* Constructor of <code>MultiPlayer</code>
*/
public MultiPlayer(final MusicPlaybackService service) {
mService = new WeakReference<MusicPlaybackService>(service);
mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ mSrtManager = new SrtManager() {
+ @Override
+ public void onTimedText(String text) {
+ mHandler.obtainMessage(LYRICS, text).sendToTarget();
+ }
+ };
}
/**
@@ -2796,10 +2829,48 @@ public class MusicPlaybackService extends Service {
public void setDataSource(final String path) {
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
if (mIsInitialized) {
+ loadSrt(path);
setNextDataSource(null);
}
}
+ private void loadSrt(final String path) {
+ mSrtManager.reset();
+
+ Uri uri = Uri.parse(path);
+ String filePath = null;
+
+ if (path.startsWith("content://")) {
+ // resolve the content resolver path to a file path
+ Cursor cursor = null;
+ try {
+ final String[] proj = {MediaStore.Audio.Media.DATA};
+ cursor = mService.get().getContentResolver().query(uri, proj,
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ filePath = cursor.getString(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+ } else {
+ filePath = uri.getPath();
+ }
+
+ if (!TextUtils.isEmpty(filePath)) {
+ final int lastIndex = filePath.lastIndexOf('.');
+ if (lastIndex != -1) {
+ String newPath = filePath.substring(0, lastIndex) + ".srt";
+ final File f = new File(newPath);
+
+ mSrtManager.initialize(mCurrentMediaPlayer, f);
+ }
+ }
+ }
+
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream
@@ -2817,6 +2888,7 @@ public class MusicPlaybackService extends Service {
player.setDataSource(path);
}
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
+
player.prepare();
} catch (final IOException todo) {
// TODO: notify the user why the file couldn't be opened
@@ -2841,6 +2913,7 @@ public class MusicPlaybackService extends Service {
* you want to play
*/
public void setNextDataSource(final String path) {
+ mNextMediaPath = null;
try {
mCurrentMediaPlayer.setNextMediaPlayer(null);
} catch (IllegalArgumentException e) {
@@ -2860,6 +2933,7 @@ public class MusicPlaybackService extends Service {
mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
if (setDataSourceImpl(mNextMediaPlayer, path)) {
+ mNextMediaPath = path;
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
} else {
if (mNextMediaPlayer != null) {
@@ -2890,6 +2964,7 @@ public class MusicPlaybackService extends Service {
*/
public void start() {
mCurrentMediaPlayer.start();
+ mSrtManager.play();
}
/**
@@ -2897,6 +2972,7 @@ public class MusicPlaybackService extends Service {
*/
public void stop() {
mCurrentMediaPlayer.reset();
+ mSrtManager.reset();
mIsInitialized = false;
}
@@ -2906,6 +2982,8 @@ public class MusicPlaybackService extends Service {
public void release() {
stop();
mCurrentMediaPlayer.release();
+ mSrtManager.release();
+ mSrtManager = null;
}
/**
@@ -2913,6 +2991,7 @@ public class MusicPlaybackService extends Service {
*/
public void pause() {
mCurrentMediaPlayer.pause();
+ mSrtManager.pause();
}
/**
@@ -2941,6 +3020,7 @@ public class MusicPlaybackService extends Service {
*/
public long seek(final long whereto) {
mCurrentMediaPlayer.seekTo((int)whereto);
+ mSrtManager.seekTo(whereto);
return whereto;
}
@@ -2998,6 +3078,8 @@ public class MusicPlaybackService extends Service {
if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = mNextMediaPlayer;
+ loadSrt(mNextMediaPath);
+ mNextMediaPath = null;
mNextMediaPlayer = null;
mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
} else {
diff --git a/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java b/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java
index 98a1be0..1a2d966 100644
--- a/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java
+++ b/src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java
@@ -16,6 +16,9 @@ import android.os.IBinder;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@@ -133,6 +136,9 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
// popup menu for pressing the menu icon
private PopupMenu mPopupMenu;
+ // Lyrics text view
+ private TextView mLyricsText;
+
private long mSelectedId = -1;
private boolean mIsPaused = false;
@@ -180,6 +186,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
mEqualizerView.initialize(getActivity());
mEqualizerGradient = mRootView.findViewById(R.id.equalizerGradient);
+ mLyricsText = (TextView) mRootView.findViewById(R.id.audio_player_lyrics);
+
return mRootView;
}
@@ -216,6 +224,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
filter.addAction(MusicPlaybackService.REFRESH);
// Listen to changes to the entire queue
filter.addAction(MusicPlaybackService.QUEUE_CHANGED);
+ // Listen for lyrics text for the audio track
+ filter.addAction(MusicPlaybackService.NEW_LYRICS);
// Register the intent filters
getActivity().registerReceiver(mPlaybackStatus, filter);
// Refresh the current time
@@ -685,6 +695,19 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
return super.onContextItemSelected(item);
}
+ public void onLyrics(String lyrics) {
+ if (TextUtils.isEmpty(lyrics)
+ || !PreferenceUtils.getInstance(getActivity()).getShowLyrics()) {
+ mLyricsText.animate().alpha(0).setDuration(200);
+ } else {
+ lyrics = lyrics.replace("\n", "<br/>");
+ Spanned span = Html.fromHtml(lyrics);
+ mLyricsText.setText(span);
+
+ mLyricsText.animate().alpha(1).setDuration(200);
+ }
+ }
+
@Override
public void onBeginSlide() {
mEqualizerView.setPanelVisible(false);
@@ -743,25 +766,28 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
*/
@Override
public void onReceive(final Context context, final Intent intent) {
+ final AudioPlayerFragment audioPlayerFragment = mReference.get();
final String action = intent.getAction();
if (action.equals(MusicPlaybackService.META_CHANGED)) {
// Current info
- mReference.get().updateNowPlayingInfo();
- mReference.get().dismissPopupMenu();
+ audioPlayerFragment.updateNowPlayingInfo();
+ audioPlayerFragment.dismissPopupMenu();
} else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
// Set the play and pause image
- mReference.get().mPlayPauseProgressButton.getPlayPauseButton().updateState();
+ audioPlayerFragment.mPlayPauseProgressButton.getPlayPauseButton().updateState();
} else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
|| action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
// Set the repeat image
- mReference.get().mRepeatButton.updateRepeatState();
+ audioPlayerFragment.mRepeatButton.updateRepeatState();
// Set the shuffle image
- mReference.get().mShuffleButton.updateShuffleState();
+ audioPlayerFragment.mShuffleButton.updateShuffleState();
// Update the queue
- mReference.get().createAndSetAdapter();
+ audioPlayerFragment.createAndSetAdapter();
} else if (action.equals(MusicPlaybackService.QUEUE_CHANGED)) {
- mReference.get().createAndSetAdapter();
+ audioPlayerFragment.createAndSetAdapter();
+ } else if (action.equals(MusicPlaybackService.NEW_LYRICS)) {
+ audioPlayerFragment.onLyrics(intent.getStringExtra("lyrics"));
}
}
}
diff --git a/src/com/cyngn/eleven/utils/PreferenceUtils.java b/src/com/cyngn/eleven/utils/PreferenceUtils.java
index 61351cc..30fef3b 100644
--- a/src/com/cyngn/eleven/utils/PreferenceUtils.java
+++ b/src/com/cyngn/eleven/utils/PreferenceUtils.java
@@ -69,6 +69,9 @@ public final class PreferenceUtils {
// datetime cutoff for determining which songs go in last added playlist
public static final String LAST_ADDED_CUTOFF = "last_added_cutoff";
+ // show lyrics option
+ public static final String SHOW_LYRICS = "show_lyrics";
+
// show visualizer flag
public static final String SHOW_VISUALIZER = "music_visualization";
@@ -307,6 +310,13 @@ public final class PreferenceUtils {
return mPreferences.getLong(LAST_ADDED_CUTOFF, 0L);
}
+ /**
+ * @return Whether we want to show lyrics
+ */
+ public final boolean getShowLyrics() {
+ return mPreferences.getBoolean(SHOW_LYRICS, true);
+ }
+
public boolean getShowVisualizer() {
return mPreferences.getBoolean(SHOW_VISUALIZER, true);
}
diff --git a/src/com/cyngn/eleven/utils/SrtManager.java b/src/com/cyngn/eleven/utils/SrtManager.java
new file mode 100644
index 0000000..fd1080d
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/SrtManager.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.utils;
+
+import android.media.MediaPlayer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * Class that helps signal when srt text comes and goes
+ */
+public abstract class SrtManager implements Handler.Callback {
+ private static final String TAG = SrtManager.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final int POST_TEXT_MSG = 0;
+
+ private ArrayList<SrtParser.SrtEntry> mEntries;
+ private Handler mHandler;
+ private HandlerThread mHandlerThread;
+
+ private Runnable mLoader;
+
+ private MediaPlayer mMediaPlayer;
+ private int mNextIndex;
+
+ public SrtManager() {
+ mHandlerThread = new HandlerThread("SrtManager",
+ android.os.Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper(), this);
+ }
+
+ public synchronized void reset() {
+ mHandler.removeMessages(POST_TEXT_MSG);
+ mHandler.removeCallbacks(mLoader);
+ mEntries = null;
+ mLoader = null;
+ mMediaPlayer = null;
+ mNextIndex = -1;
+
+ // post a null timed text to clear
+ onTimedText(null);
+ }
+
+ public synchronized void release() {
+ reset();
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ }
+
+ public synchronized void initialize(final MediaPlayer player, final File f) {
+ if (player == null || f == null) {
+ throw new IllegalArgumentException("Must have a valid player and file");
+ }
+
+ reset();
+
+ if (!f.exists()) {
+ return;
+ }
+
+ mMediaPlayer = player;
+
+ mLoader = new Runnable() {
+ @Override
+ public void run() {
+ onLoaded(this, SrtParser.getSrtEntries(f));
+ }
+ };
+
+ mHandler.post(mLoader);
+ }
+
+ public synchronized void seekTo(long timeMs) {
+ mHandler.removeMessages(POST_TEXT_MSG);
+
+ mNextIndex = 0;
+
+ if (mEntries != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Seeking to: " + timeMs);
+ }
+
+ // find the first entry after the current time and set mNextIndex to the one before that
+ for (int i = 0; i < mEntries.size(); i++) {
+ mNextIndex = i;
+ if (i + 1 < mEntries.size() && mEntries.get(i + 1).mStartTimeMs > timeMs) {
+ break;
+ }
+ }
+
+ postNextTimedText();
+ }
+ }
+
+ public synchronized void pause() {
+ mHandler.removeMessages(POST_TEXT_MSG);
+ }
+
+ public synchronized void play() {
+ postNextTimedText();
+ }
+
+ private synchronized void onLoaded(Runnable r, ArrayList<SrtParser.SrtEntry> entries) {
+ // if this is the same loader
+ if (r == mLoader) {
+ mEntries = entries;
+ if (mEntries != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Loaded: " + entries.size() + " number of entries");
+ }
+
+ try {
+ seekTo(mMediaPlayer.getCurrentPosition());
+ } catch(IllegalStateException e) {
+ Log.d(TAG, "illegal state but failing silently");
+ reset();
+ }
+ }
+ }
+ }
+
+ private synchronized void postNextTimedText() {
+ if (mEntries != null) {
+ long timeMs = 0;
+ try {
+ timeMs = mMediaPlayer.getCurrentPosition();
+ } catch (IllegalStateException e) {
+ Log.d(TAG, "illegal state - probably because media player has been " +
+ "stopped/released. failing silently");
+ return;
+ }
+
+ String currentMessage = null;
+ long targetTime = -1;
+
+ // shift mNextIndex until it hits the next item we want
+ while (mNextIndex < mEntries.size() && mEntries.get(mNextIndex).mStartTimeMs < timeMs) {
+ mNextIndex++;
+ }
+
+ // if the previous entry is valid, set the message and target time
+ if (mNextIndex > 0 && entrySurroundsTime(mEntries.get(mNextIndex - 1), timeMs)) {
+ currentMessage = mEntries.get(mNextIndex - 1).mLine;
+ targetTime = mEntries.get(mNextIndex - 1).mEndTimeMs;
+ }
+
+ onTimedText(currentMessage);
+
+ // if our next index is valid, and we don't have a target time, set it
+ if (mNextIndex < mEntries.size() && targetTime == -1) {
+ targetTime = mEntries.get(mNextIndex).mStartTimeMs;
+ }
+
+ // if we have a targeted time entry and we are playing, then queue up a delayed message
+ if (targetTime >= 0 && mMediaPlayer.isPlaying()) {
+ mHandler.removeMessages(POST_TEXT_MSG);
+
+ long delay = targetTime - timeMs;
+ mHandler.sendEmptyMessageDelayed(POST_TEXT_MSG, delay);
+
+ if (DEBUG && mNextIndex < mEntries.size()) {
+ Log.d(TAG, "Preparing next message: " + delay + "ms from now with msg: " +
+ mEntries.get(mNextIndex).mLine);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case POST_TEXT_MSG:
+ postNextTimedText();
+ return true;
+ }
+
+ return false;
+ }
+
+ private static boolean entrySurroundsTime(SrtParser.SrtEntry entry, long time) {
+ return entry.mStartTimeMs <= time && entry.mEndTimeMs >= time;
+ }
+
+ public abstract void onTimedText(String txt);
+}
diff --git a/src/com/cyngn/eleven/utils/SrtParser.java b/src/com/cyngn/eleven/utils/SrtParser.java
new file mode 100644
index 0000000..749f80d
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/SrtParser.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.utils;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class SrtParser {
+ private static final String TAG = SrtParser.class.getSimpleName();
+
+ public static class SrtEntry {
+ public long mStartTimeMs;
+ public long mEndTimeMs;
+ String mLine;
+ }
+
+ /**
+ * The SubRip file format should contain entries that follow the following format:
+ *
+ * 1. A numeric counter identifying each sequential subtitle
+ * 2. The time that the subtitle should appear on the screen, followed by --> and the time it
+ * should disappear
+ * 3. Subtitle text itself on one or more lines
+ * 4. A blank line containing no text, indicating the end of this subtitle
+ *
+ * The timecode format should be hours:minutes:seconds,milliseconds with time units fixed to two
+ * zero-padded digits and fractions fixed to three zero-padded digits (00:00:00,000).
+ */
+ public static ArrayList<SrtEntry> getSrtEntries(File f) {
+ ArrayList<SrtEntry> ret = null;
+ FileReader reader = null;
+ BufferedReader br = null;
+
+ try {
+ reader = new FileReader(f);
+ br = new BufferedReader(reader);
+
+ String header;
+ // since we don't really care about the 1st line of each entry (the # val) then read
+ // and discard it
+ while ((header = br.readLine()) != null) {
+ // discard subtitle number
+ header = br.readLine();
+ if (header == null) {
+ break;
+ }
+
+ SrtEntry entry = new SrtEntry();
+
+ String[] startEnd = header.split("-->");
+ entry.mStartTimeMs = parseMs(startEnd[0]);
+ entry.mEndTimeMs = parseMs(startEnd[1]);
+
+ StringBuilder subtitleBuilder = new StringBuilder("");
+ String s = br.readLine();
+
+ if (!TextUtils.isEmpty(s)) {
+ subtitleBuilder.append(s);
+
+ while (!((s = br.readLine()) == null || s.trim().equals(""))) {
+ subtitleBuilder.append("\n" + s);
+ }
+ }
+
+ entry.mLine = subtitleBuilder.toString();
+
+ if (ret == null) {
+ ret = new ArrayList<SrtEntry>();
+ }
+
+ ret.add(entry);
+ }
+ } catch (IOException ioe) {
+ // shouldn't happen
+ Log.e(TAG, ioe.getMessage(), ioe);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // if the time is malformed
+ Log.e(TAG, e.getMessage());
+ } finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ private static long parseMs(String in) {
+ String[] timeArray = in.split(":");
+ long hours = Long.parseLong(timeArray[0].trim());
+ long minutes = Long.parseLong(timeArray[1].trim());
+
+ String[] secondTimeArray = timeArray[2].split(",");
+
+ long seconds = Long.parseLong(secondTimeArray[0].trim());
+ long millies = Long.parseLong(secondTimeArray[1].trim());
+
+ return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies;
+ }
+}