Commit ad606c2e authored by linus_lee's avatar linus_lee
Browse files

Eleven: Add Lyric (srt) support

https://cyanogen.atlassian.net/browse/MUSIC-186

Change-Id: I8fec1c61d69dca06be30ccebf9c996d7fac2ae75
parent 1f86c572
......@@ -49,6 +49,21 @@
android:clipChildren="false"
android:clipToPadding="false"
android:visibility="visible" />
<TextView
android:id="@+id/audio_player_lyrics"
android:layout_gravity="center"
android:gravity="center"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="66dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:background="@color/lyrics_background_color"
android:textColor="@color/white"
android:textSize="@dimen/text_size_small"/>
</com.cyngn.eleven.widgets.SquareFrame>
<RelativeLayout
......
......@@ -112,4 +112,7 @@
<color name="widget_divider">#373737</color>
<!-- 80% opacity white -->
<color name="widget_text">#ccffffff</color>
<!-- Background Lyrics Color -->
<color name="lyrics_background_color">#b2000000</color>
</resources>
......@@ -138,6 +138,8 @@
<string name="settings_download_artist_images_title">Download missing artist images</string>
<string name="settings_general_category">General</string>
<string name="settings_show_music_visualization_title">Show music visualization</string>
<string name="settings_show_lyrics_title">Show song lyrics</string>
<string name="settings_show_lyrics_summary">For songs that have an srt file</string>
<!-- App widget -->
<string name="app_widget_small">Music: 4 \u00d7 1</string>
......
......@@ -43,6 +43,13 @@
android:defaultValue="true"
android:key="music_visualization"
android:title="@string/settings_show_music_visualization_title" />
<!-- Show Lyrics -->
<CheckBoxPreference
android:defaultValue="true"
android:key="show_lyrics"
android:title="@string/settings_show_lyrics_title"
android:summary="@string/settings_show_lyrics_summary"/>
</PreferenceCategory>
<!-- Storage catetory -->
<PreferenceCategory android:title="@string/settings_storage_category" >
......
......@@ -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;
......@@ -188,6 +190,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
*/
......@@ -286,6 +293,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)
*/
......@@ -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 {
......
......@@ -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"));
}
}
}
......
......@@ -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);
}
......
/*
* 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);
}
/*
* 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
*