diff options
author | linus_lee <llee@cyngn.com> | 2014-11-19 12:47:04 -0800 |
---|---|---|
committer | linus_lee <llee@cyngn.com> | 2014-12-09 11:59:39 -0800 |
commit | ad606c2e575a45174ac81f18ee34985616458d0c (patch) | |
tree | 6042a94864a3ce283894a46f85331aae8830f443 /src | |
parent | 1f86c5725d8ca216040199be2a1c664d1861eed8 (diff) | |
download | android_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')
-rw-r--r-- | src/com/cyngn/eleven/MusicPlaybackService.java | 82 | ||||
-rw-r--r-- | src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java | 40 | ||||
-rw-r--r-- | src/com/cyngn/eleven/utils/PreferenceUtils.java | 10 | ||||
-rw-r--r-- | src/com/cyngn/eleven/utils/SrtManager.java | 199 | ||||
-rw-r--r-- | src/com/cyngn/eleven/utils/SrtParser.java | 119 |
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; + } +} |