package org.music.player; import org.music.player.R; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.media.MediaMetadataRetriever; import android.os.Build; import android.os.Bundle; import android.os.Message; import android.text.format.DateUtils; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; /** * The primary playback screen with playback controls and large cover display. */ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.OnSeekBarChangeListener , View.OnLongClickListener { public static final int DISPLAY_INFO_OVERLAP = 0; public static final int DISPLAY_INFO_BELOW = 1; public static final int DISPLAY_INFO_WIDGETS = 2; private TextView mOverlayText; private View mControlsTop; private View mControlsBottom; private SeekBar mSeekBar; private TableLayout mInfoTable; private TextView mElapsedView; private TextView mDurationView; private TextView mQueuePosView; private TextView mTitle; private TextView mAlbum; private TextView mArtist; /** * True if the controls are visible (play, next, seek bar, etc). */ private boolean mControlsVisible; /** * True if the extra info is visible. */ private boolean mExtraInfoVisible; /** * Current song duration in milliseconds. */ private long mDuration; private boolean mSeekBarTracking; private boolean mPaused; /** * The current display mode, which determines layout and cover render style. */ private int mDisplayMode; private Action mCoverPressAction; private Action mCoverLongPressAction; /** * Cached StringBuilder for formatting track position. */ private final StringBuilder mTimeBuilder = new StringBuilder(); /** * The currently playing song. */ private Song mCurrentSong; private String mGenre; private TextView mGenreView; private String mTrack; private TextView mTrackView; private String mYear; private TextView mYearView; private String mComposer; private TextView mComposerView; private String mFormat; private TextView mFormatView; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setTitle(R.string.playback_view); SharedPreferences settings = PlaybackService.getSettings(this); int displayMode = Integer.parseInt(settings.getString(PrefKeys.DISPLAY_MODE, "2")); mDisplayMode = displayMode; int layout = R.layout.full_playback; int coverStyle; switch (displayMode) { default: Log.w("VanillaMusic", "Invalid display mode given. Defaulting to widget mode."); // fall through case DISPLAY_INFO_WIDGETS: coverStyle = CoverBitmap.STYLE_NO_INFO; layout = R.layout.full_playback_alt; break; case DISPLAY_INFO_OVERLAP: coverStyle = CoverBitmap.STYLE_OVERLAPPING_BOX; break; case DISPLAY_INFO_BELOW: coverStyle = CoverBitmap.STYLE_INFO_BELOW; break; } setContentView(layout); CoverView coverView = (CoverView)findViewById(R.id.cover_view); coverView.setup(mLooper, this, coverStyle); coverView.setOnClickListener(this); coverView.setOnLongClickListener(this); mCoverView = coverView; mControlsBottom = findViewById(R.id.controls_bottom); View previousButton = findViewById(R.id.previous); previousButton.setOnClickListener(this); mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause); mPlayPauseButton.setOnClickListener(this); View nextButton = findViewById(R.id.next); nextButton.setOnClickListener(this); TableLayout table = (TableLayout)findViewById(R.id.info_table); if (table != null) { table.setOnClickListener(this); table.setOnLongClickListener(this); mInfoTable = table; } mTitle = (TextView)findViewById(R.id.title); mAlbum = (TextView)findViewById(R.id.album); mArtist = (TextView)findViewById(R.id.artist); mControlsTop = findViewById(R.id.controls_top); mElapsedView = (TextView)findViewById(R.id.elapsed); mDurationView = (TextView)findViewById(R.id.duration); mSeekBar = (SeekBar)findViewById(R.id.seek_bar); mSeekBar.setMax(1000); mSeekBar.setOnSeekBarChangeListener(this); mQueuePosView = (TextView)findViewById(R.id.queue_pos); mGenreView = (TextView)findViewById(R.id.genre); mTrackView = (TextView)findViewById(R.id.track); mYearView = (TextView)findViewById(R.id.year); mComposerView = (TextView)findViewById(R.id.composer); mFormatView = (TextView)findViewById(R.id.format); mShuffleButton = (ImageButton)findViewById(R.id.shuffle); mShuffleButton.setOnClickListener(this); registerForContextMenu(mShuffleButton); mEndButton = (ImageButton)findViewById(R.id.end_action); mEndButton.setOnClickListener(this); registerForContextMenu(mEndButton); setControlsVisible(settings.getBoolean(PrefKeys.VISIBLE_CONTROLS, true)); setExtraInfoVisible(settings.getBoolean(PrefKeys.VISIBLE_EXTRA_INFO, false)); setDuration(0); } @Override public void onStart() { super.onStart(); SharedPreferences settings = PlaybackService.getSettings(this); if (mDisplayMode != Integer.parseInt(settings.getString(PrefKeys.DISPLAY_MODE, "2"))) { finish(); startActivity(new Intent(this, FullPlaybackActivity.class)); } mCoverPressAction = Action.getAction(settings, PrefKeys.COVER_PRESS_ACTION, Action.ToggleControls); mCoverLongPressAction = Action.getAction(settings, PrefKeys.COVER_LONGPRESS_ACTION, Action.PlayPause); } @Override public void onResume() { super.onResume(); mPaused = false; updateElapsedTime(); } @Override public void onPause() { super.onPause(); mPaused = true; } /** * Hide the message overlay, if it exists. */ private void hideMessageOverlay() { if (mOverlayText != null) mOverlayText.setVisibility(View.GONE); } /** * Show some text in a message overlay. * * @param text Resource id of the text to show. */ private void showOverlayMessage(int text) { if (mOverlayText == null) { TextView view = new TextView(this); view.setBackgroundColor(Color.BLACK); view.setTextColor(Color.WHITE); view.setGravity(Gravity.CENTER); view.setPadding(25, 25, 25, 25); // Make the view clickable so it eats touch events view.setClickable(true); view.setOnClickListener(this); addContentView(view, new ViewGroup.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); mOverlayText = view; } else { mOverlayText.setVisibility(View.VISIBLE); } mOverlayText.setText(text); } @Override protected void onStateChange(int state, int toggled) { super.onStateChange(state, toggled); if ((toggled & (PlaybackService.FLAG_NO_MEDIA|PlaybackService.FLAG_EMPTY_QUEUE)) != 0) { if ((state & PlaybackService.FLAG_NO_MEDIA) != 0) { showOverlayMessage(R.string.no_songs); } else if ((state & PlaybackService.FLAG_EMPTY_QUEUE) != 0) { showOverlayMessage(R.string.empty_queue); } else { hideMessageOverlay(); } } if ((state & PlaybackService.FLAG_PLAYING) != 0) updateElapsedTime(); if (mQueuePosView != null) updateQueuePosition(); } @Override protected void onSongChange(Song song) { super.onSongChange(song); setDuration(song == null ? 0 : song.duration); if (mTitle != null) { if (song == null) { mTitle.setText(null); mAlbum.setText(null); mArtist.setText(null); } else { mTitle.setText(song.title); mAlbum.setText(song.album); mArtist.setText(song.artist); } updateQueuePosition(); } mCurrentSong = song; updateElapsedTime(); if (mExtraInfoVisible) { mHandler.sendEmptyMessage(MSG_LOAD_EXTRA_INFO); } } /** * Update the queue position display. mQueuePos must not be null. */ private void updateQueuePosition() { if (PlaybackService.finishAction(mState) == SongTimeline.FINISH_RANDOM) { // Not very useful in random mode; it will always show something // like 11/13 since the timeline is trimmed to 10 previous songs. // So just hide it. mQueuePosView.setText(null); } else { PlaybackService service = PlaybackService.get(this); mQueuePosView.setText((service.getTimelinePosition() + 1) + "/" + service.getTimelineLength()); } mInfoTable.requestLayout(); // ensure queue pos column has enough room } @Override public void onPositionInfoChanged() { if (mQueuePosView != null) mUiHandler.sendEmptyMessage(MSG_UPDATE_POSITION); } /** * Update the current song duration fields. * * @param duration The new duration, in milliseconds. */ private void setDuration(long duration) { mDuration = duration; mDurationView.setText(DateUtils.formatElapsedTime(mTimeBuilder, duration / 1000)); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { menu.add(0, MENU_LIBRARY, 0, R.string.library).setIcon(R.drawable.ic_menu_music_library); } super.onCreateOptionsMenu(menu); menu.add(0, MENU_CLEAR_QUEUE, 0, R.string.clear_queue).setIcon(R.drawable.ic_menu_close_clear_cancel); menu.add(0, MENU_ENQUEUE_ALBUM, 0, R.string.enqueue_current_album).setIcon(R.drawable.ic_menu_add); menu.add(0, MENU_ENQUEUE_ARTIST, 0, R.string.enqueue_current_artist).setIcon(R.drawable.ic_menu_add); menu.add(0, MENU_ENQUEUE_GENRE, 0, R.string.enqueue_current_genre).setIcon(R.drawable.ic_menu_add); menu.add(0, MENU_TOGGLE_CONTROLS, 0, R.string.toggle_controls); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: case MENU_LIBRARY: openLibrary(null); break; case MENU_ENQUEUE_ALBUM: PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ALBUM); break; case MENU_ENQUEUE_ARTIST: PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ARTIST); break; case MENU_ENQUEUE_GENRE: PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_GENRE); break; case MENU_CLEAR_QUEUE: PlaybackService.get(this).clearQueue(); break; case MENU_TOGGLE_CONTROLS: setControlsVisible(!mControlsVisible); mHandler.sendEmptyMessage(MSG_SAVE_CONTROLS); break; default: return super.onOptionsItemSelected(item); } return true; } @Override public boolean onSearchRequested() { openLibrary(null); return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_RIGHT: shiftCurrentSong(SongTimeline.SHIFT_NEXT_SONG); findViewById(R.id.next).requestFocus(); return true; case KeyEvent.KEYCODE_DPAD_LEFT: shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_SONG); findViewById(R.id.previous).requestFocus(); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: setControlsVisible(!mControlsVisible); mHandler.sendEmptyMessage(MSG_SAVE_CONTROLS); return true; } return super.onKeyUp(keyCode, event); } /** * Update seek bar progress and schedule another update in one second */ private void updateElapsedTime() { long position = PlaybackService.hasInstance() ? PlaybackService.get(this).getPosition() : 0; if (!mSeekBarTracking) { long duration = mDuration; mSeekBar.setProgress(duration == 0 ? 0 : (int)(1000 * position / duration)); } mElapsedView.setText(DateUtils.formatElapsedTime(mTimeBuilder, position / 1000)); if (!mPaused && mControlsVisible && (mState & PlaybackService.FLAG_PLAYING) != 0) { // Try to update right after the duration increases by one second long next = 1050 - position % 1000; mUiHandler.removeMessages(MSG_UPDATE_PROGRESS); mUiHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, next); } } /** * Set the visibility of the controls views. * * @param visible True to show, false to hide */ private void setControlsVisible(boolean visible) { int mode = visible ? View.VISIBLE : View.GONE; mControlsTop.setVisibility(mode); mControlsBottom.setVisibility(mode); mControlsVisible = visible; if (visible) { mPlayPauseButton.requestFocus(); updateElapsedTime(); } } /** * Set the visibility of the extra metadata view. * * @param visible True to show, false to hide */ private void setExtraInfoVisible(boolean visible) { TableLayout table = mInfoTable; if (table == null) return; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) visible = false; table.setColumnCollapsed(0, !visible); // Make title, album, and artist multi-line when extra info is visible boolean singleLine = !visible; for (int i = 0; i != 3; ++i) { TableRow row = (TableRow)table.getChildAt(i); ((TextView)row.getChildAt(1)).setSingleLine(singleLine); } // toggle visibility of all but the first three rows (the title/artist/ // album rows) and the last row (the seek bar) int visibility = visible ? View.VISIBLE : View.GONE; for (int i = table.getChildCount() - 1; --i != 2; ) { table.getChildAt(i).setVisibility(visibility); } mExtraInfoVisible = visible; if (visible && !mHandler.hasMessages(MSG_LOAD_EXTRA_INFO)) { mHandler.sendEmptyMessage(MSG_LOAD_EXTRA_INFO); } } /** * Retrieve the extra metadata for the current song. */ private void loadExtraInfo() { Song song = mCurrentSong; if (song == null) { mGenre = null; mTrack = null; mYear = null; mComposer = null; mFormat = null; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { CompatMetadata data = new CompatMetadata(song.path); mGenre = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE); mTrack = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER); String composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER); if (composer == null) composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER); mComposer = composer; String year = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR); if (year == null || "0".equals(year)) { year = null; } else { int dash = year.indexOf('-'); if (dash != -1) year = year.substring(0, dash); } mYear = year; StringBuilder sb = new StringBuilder(12); sb.append(decodeMimeType(data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE))); String bitrate = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE); if (bitrate != null && bitrate.length() > 3) { sb.append(' '); sb.append(bitrate.substring(0, bitrate.length() - 3)); sb.append("kbps"); } mFormat = sb.toString(); data.release(); } mUiHandler.sendEmptyMessage(MSG_COMMIT_INFO); } /** * Decode the given mime type into a more human-friendly description. */ private static String decodeMimeType(String mime) { if ("audio/mpeg".equals(mime)) { return "MP3"; } else if ("audio/mp4".equals(mime)) { return "AAC"; } else if ("audio/vorbis".equals(mime)) { return "Ogg Vorbis"; } else if ("audio/flac".equals(mime)) { return "FLAC"; } return mime; } /** * Update the seekbar progress with the current song progress. This must be * called on the UI Handler. */ private static final int MSG_UPDATE_PROGRESS = 10; /** * Save the hidden_controls preference to storage. */ private static final int MSG_SAVE_CONTROLS = 14; /** * Call {@link #loadExtraInfo()}. */ private static final int MSG_LOAD_EXTRA_INFO = 15; /** * Pass obj to mExtraInfo.setText() */ private static final int MSG_COMMIT_INFO = 16; /** * Calls {@link #updateQueuePosition()}. */ private static final int MSG_UPDATE_POSITION = 17; @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SAVE_CONTROLS: { SharedPreferences.Editor editor = PlaybackService.getSettings(this).edit(); editor.putBoolean("visible_controls", mControlsVisible); editor.putBoolean("visible_extra_info", mExtraInfoVisible); editor.commit(); break; } case MSG_UPDATE_PROGRESS: updateElapsedTime(); break; case MSG_LOAD_EXTRA_INFO: loadExtraInfo(); break; case MSG_COMMIT_INFO: { mGenreView.setText(mGenre); mTrackView.setText(mTrack); mYearView.setText(mYear); mComposerView.setText(mComposer); mFormatView.setText(mFormat); break; } case MSG_UPDATE_POSITION: updateQueuePosition(); break; default: return super.handleMessage(message); } return true; } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { mElapsedView.setText(DateUtils.formatElapsedTime(mTimeBuilder, progress * mDuration / 1000000)); PlaybackService.get(this).seekToProgress(progress); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { mSeekBarTracking = true; } @Override public void onStopTrackingTouch(SeekBar seekBar) { mSeekBarTracking = false; } public void performAction(Action action) { if (action == Action.ToggleControls) { setControlsVisible(!mControlsVisible); mHandler.sendEmptyMessage(MSG_SAVE_CONTROLS); } else { PlaybackService.get(this).performAction(action, this); } } @Override public void onClick(View view) { if (view == mOverlayText && (mState & PlaybackService.FLAG_EMPTY_QUEUE) != 0) { setState(PlaybackService.get(this).setFinishAction(SongTimeline.FINISH_RANDOM)); } else if (view == mCoverView) { performAction(mCoverPressAction); } else if (view.getId() == R.id.info_table) { openLibrary(mCurrentSong); } else { super.onClick(view); } } @Override public boolean onLongClick(View view) { switch (view.getId()) { case R.id.cover_view: performAction(mCoverLongPressAction); break; case R.id.info_table: setExtraInfoVisible(!mExtraInfoVisible); mHandler.sendEmptyMessage(MSG_SAVE_CONTROLS); break; default: return false; } return true; } }