2 * Copyright (C) 2014 Cyanogen, Inc.
4 package com.cyngn.eleven.ui.fragments;
6 import android.app.Activity;
7 import android.content.BroadcastReceiver;
8 import android.content.ComponentName;
9 import android.content.Context;
10 import android.content.Intent;
11 import android.content.IntentFilter;
12 import android.content.ServiceConnection;
13 import android.media.AudioManager;
14 import android.net.Uri;
15 import android.os.Bundle;
16 import android.os.Handler;
17 import android.os.IBinder;
18 import android.os.Message;
19 import android.os.SystemClock;
20 import android.support.v4.app.Fragment;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import android.widget.ImageView;
25 import android.widget.LinearLayout;
26 import android.widget.SeekBar;
27 import android.widget.TextView;
29 import android.provider.MediaStore.Audio.Playlists;
30 import android.provider.MediaStore.Audio.Albums;
31 import android.provider.MediaStore.Audio.Artists;
32 import com.cyngn.eleven.MusicPlaybackService;
33 import com.cyngn.eleven.R;
34 import com.cyngn.eleven.cache.ImageFetcher;
35 import com.cyngn.eleven.utils.ApolloUtils;
36 import com.cyngn.eleven.utils.MusicUtils;
37 import com.cyngn.eleven.utils.NavUtils;
38 import com.cyngn.eleven.widgets.PlayPauseButton;
39 import com.cyngn.eleven.widgets.RepeatButton;
40 import com.cyngn.eleven.widgets.RepeatingImageButton;
41 import com.cyngn.eleven.widgets.ShuffleButton;
43 import java.lang.ref.WeakReference;
45 import static com.cyngn.eleven.utils.MusicUtils.mService;
47 public class AudioPlayerFragment extends Fragment implements ServiceConnection,
48 SeekBar.OnSeekBarChangeListener {
51 private ViewGroup mRootView;
53 // Message to refresh the time
54 private static final int REFRESH_TIME = 1;
57 private MusicUtils.ServiceToken mToken;
59 // Play and pause button
60 private PlayPauseButton mPlayPauseButton;
63 private RepeatButton mRepeatButton;
66 private ShuffleButton mShuffleButton;
69 private RepeatingImageButton mPreviousButton;
72 private RepeatingImageButton mNextButton;
75 private TextView mTrackName;
78 private TextView mArtistName;
81 private ImageView mAlbumArt;
84 private ImageView mAlbumArtSmall;
87 private TextView mCurrentTime;
90 private TextView mTotalTime;
93 private SeekBar mProgress;
96 private PlaybackStatus mPlaybackStatus;
98 // Handler used to update the current time
99 private TimeHandler mTimeHandler;
102 private LinearLayout mAudioPlayerHeader;
105 private ImageFetcher mImageFetcher;
107 private long mPosOverride = -1;
109 private long mStartSeekPos = 0;
111 private long mLastSeekEventTime;
113 private long mLastShortSeekEventTime;
115 private boolean mIsPaused = false;
117 private boolean mFromTouch = false;
120 public void onActivityCreated(Bundle savedInstanceState) {
121 super.onActivityCreated(savedInstanceState);
123 // Control the media volume
124 getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
126 // Bind Apollo's service
127 mToken = MusicUtils.bindToService(getActivity(), this);
129 // Initialize the image fetcher/cache
130 mImageFetcher = ApolloUtils.getImageFetcher(getActivity());
132 // Initialize the handler used to update the current time
133 mTimeHandler = new TimeHandler(this);
135 // Initialize the broadcast receiver
136 mPlaybackStatus = new PlaybackStatus(this);
143 public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
144 final Bundle savedInstanceState) {
145 // The View for the fragment's UI
146 mRootView = (ViewGroup) inflater.inflate(R.layout.activity_player_fragment, null);
147 initPlaybackControls();
155 public void onServiceConnected(final ComponentName name, final IBinder service) {
156 // Check whether we were asked to start any playback
158 // Set the playback drawables
159 updatePlaybackControls();
161 updateNowPlayingInfo();
162 // Update the favorites icon
164 // invalidateOptionsMenu();
168 public void onServiceDisconnected(ComponentName name) {
175 public void onProgressChanged(final SeekBar bar, final int progress, final boolean fromuser) {
176 if (!fromuser || mService == null) {
179 final long now = SystemClock.elapsedRealtime();
180 if (now - mLastSeekEventTime > 250) {
181 mLastSeekEventTime = now;
182 mLastShortSeekEventTime = now;
183 mPosOverride = MusicUtils.duration() * progress / 1000;
184 MusicUtils.seek(mPosOverride);
186 // refreshCurrentTime();
189 } else if (now - mLastShortSeekEventTime > 5) {
190 mLastShortSeekEventTime = now;
191 mPosOverride = MusicUtils.duration() * progress / 1000;
192 refreshCurrentTimeText(mPosOverride);
200 public void onStartTrackingTouch(final SeekBar bar) {
201 mLastSeekEventTime = 0;
203 mCurrentTime.setVisibility(View.VISIBLE);
210 public void onStopTrackingTouch(final SeekBar bar) {
211 if (mPosOverride != -1) {
212 MusicUtils.seek(mPosOverride);
218 public void onDelete(long[] ids) {
219 if (MusicUtils.getQueue().length == 0) {
220 NavUtils.goHome(getActivity());
224 public void onVisible() {
225 // Set the playback drawables
226 updatePlaybackControls();
228 updateNowPlayingInfo();
231 public void onHidden() {
236 public void onStart() {
239 final IntentFilter filter = new IntentFilter();
240 // Play and pause changes
241 filter.addAction(MusicPlaybackService.PLAYSTATE_CHANGED);
242 // Shuffle and repeat changes
243 filter.addAction(MusicPlaybackService.SHUFFLEMODE_CHANGED);
244 filter.addAction(MusicPlaybackService.REPEATMODE_CHANGED);
246 filter.addAction(MusicPlaybackService.META_CHANGED);
247 // Update a list, probably the playlist fragment's
248 filter.addAction(MusicPlaybackService.REFRESH);
249 getActivity().registerReceiver(mPlaybackStatus, filter);
250 // Refresh the current time
251 final long next = refreshCurrentTime();
252 queueNextRefresh(next);
256 public void onStop() {
259 mImageFetcher.flush();
263 public void onDestroy() {
267 mTimeHandler.removeMessages(REFRESH_TIME);
268 // Unbind from the service
269 if (mService != null) {
270 MusicUtils.unbindFromService(mToken);
274 // Unregister the receiver
276 getActivity().unregisterReceiver(mPlaybackStatus);
277 } catch (final Throwable e) {
283 * Initializes the items in the now playing screen
285 @SuppressWarnings("deprecation")
286 private void initPlaybackControls() {
287 // Now playing header
288 mAudioPlayerHeader = (LinearLayout)mRootView.findViewById(R.id.audio_player_header);
289 // Opens the currently playing album profile
290 mAudioPlayerHeader.setOnClickListener(mOpenAlbumProfile);
292 // Play and pause button
293 mPlayPauseButton = (PlayPauseButton)mRootView.findViewById(R.id.action_button_play);
295 mShuffleButton = (ShuffleButton)mRootView.findViewById(R.id.action_button_shuffle);
297 mRepeatButton = (RepeatButton)mRootView.findViewById(R.id.action_button_repeat);
299 mPreviousButton = (RepeatingImageButton)mRootView.findViewById(R.id.action_button_previous);
301 mNextButton = (RepeatingImageButton)mRootView.findViewById(R.id.action_button_next);
303 mTrackName = (TextView)mRootView.findViewById(R.id.audio_player_track_name);
305 mArtistName = (TextView)mRootView.findViewById(R.id.audio_player_artist_name);
307 mAlbumArt = (ImageView)mRootView.findViewById(R.id.audio_player_album_art);
309 mAlbumArtSmall = (ImageView)mRootView.findViewById(R.id.audio_player_switch_album_art);
311 mCurrentTime = (TextView)mRootView.findViewById(R.id.audio_player_current_time);
313 mTotalTime = (TextView)mRootView.findViewById(R.id.audio_player_total_time);
315 mProgress = (SeekBar)mRootView.findViewById(android.R.id.progress);
317 // Set the repeat listner for the previous button
318 mPreviousButton.setRepeatListener(mRewindListener);
319 // Set the repeat listner for the next button
320 mNextButton.setRepeatListener(mFastForwardListener);
321 // Update the progress
322 mProgress.setOnSeekBarChangeListener(this);
326 * Sets the track name, album name, and album art.
328 private void updateNowPlayingInfo() {
329 // Set the track name
330 mTrackName.setText(MusicUtils.getTrackName());
331 // Set the artist name
332 mArtistName.setText(MusicUtils.getArtistName());
333 // Set the total time
334 mTotalTime.setText(MusicUtils.makeTimeString(getActivity(), MusicUtils.duration() / 1000));
336 mImageFetcher.loadCurrentArtwork(mAlbumArt);
337 // Set the small artwork
338 mImageFetcher.loadCurrentArtwork(mAlbumArtSmall);
339 // Update the current time
344 private long parseIdFromIntent(Intent intent, String longKey,
345 String stringKey, long defaultId) {
346 long id = intent.getLongExtra(longKey, -1);
348 String idString = intent.getStringExtra(stringKey);
349 if (idString != null) {
351 id = Long.parseLong(idString);
352 } catch (NumberFormatException e) {
361 * Checks whether the passed intent contains a playback request,
362 * and starts playback if that's the case
363 * @return true if the intent was consumed
365 public boolean startPlayback() {
366 Intent intent = getActivity().getIntent();
368 if (intent == null || mService == null || getActivity() == null) {
372 Uri uri = intent.getData();
373 String mimeType = intent.getType();
374 boolean handled = false;
376 if (uri != null && uri.toString().length() > 0) {
377 MusicUtils.playFile(getActivity(), uri);
379 } else if (Playlists.CONTENT_TYPE.equals(mimeType)) {
380 long id = parseIdFromIntent(intent, "playlistId", "playlist", -1);
382 MusicUtils.playPlaylist(getActivity(), id);
385 } else if (Albums.CONTENT_TYPE.equals(mimeType)) {
386 long id = parseIdFromIntent(intent, "albumId", "album", -1);
388 int position = intent.getIntExtra("position", 0);
389 MusicUtils.playAlbum(getActivity(), id, position);
392 } else if (Artists.CONTENT_TYPE.equals(mimeType)) {
393 long id = parseIdFromIntent(intent, "artistId", "artist", -1);
395 int position = intent.getIntExtra("position", 0);
396 MusicUtils.playArtist(getActivity(), id, position);
402 // Make sure to process intent only once
403 getActivity().setIntent(new Intent());
405 // TODO: Refresh queue or have it self-aware
413 * Sets the correct drawable states for the playback controls.
415 private void updatePlaybackControls() {
416 // Set the play and pause image
417 mPlayPauseButton.updateState();
418 // Set the shuffle image
419 mShuffleButton.updateShuffleState();
420 // Set the repeat image
421 mRepeatButton.updateRepeatState();
425 * @param delay When to update
427 private void queueNextRefresh(final long delay) {
429 final Message message = mTimeHandler.obtainMessage(REFRESH_TIME);
430 mTimeHandler.removeMessages(REFRESH_TIME);
431 mTimeHandler.sendMessageDelayed(message, delay);
436 * Used to scan backwards in time through the curren track
438 * @param repcnt The repeat count
439 * @param delta The long press duration
441 private void scanBackward(final int repcnt, long delta) {
442 if (mService == null) {
446 mStartSeekPos = MusicUtils.position();
447 mLastSeekEventTime = 0;
450 // seek at 10x speed for the first 5 seconds
453 // seek at 40x after that
454 delta = 50000 + (delta - 5000) * 40;
456 long newpos = mStartSeekPos - delta;
458 // move to previous track
459 MusicUtils.previous(getActivity());
460 final long duration = MusicUtils.duration();
461 mStartSeekPos += duration;
464 if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
465 MusicUtils.seek(newpos);
466 mLastSeekEventTime = delta;
469 mPosOverride = newpos;
473 refreshCurrentTime();
478 * Used to scan forwards in time through the curren track
480 * @param repcnt The repeat count
481 * @param delta The long press duration
483 private void scanForward(final int repcnt, long delta) {
484 if (mService == null) {
488 mStartSeekPos = MusicUtils.position();
489 mLastSeekEventTime = 0;
492 // seek at 10x speed for the first 5 seconds
495 // seek at 40x after that
496 delta = 50000 + (delta - 5000) * 40;
498 long newpos = mStartSeekPos + delta;
499 final long duration = MusicUtils.duration();
500 if (newpos >= duration) {
501 // move to next track
503 mStartSeekPos -= duration; // is OK to go negative
506 if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
507 MusicUtils.seek(newpos);
508 mLastSeekEventTime = delta;
511 mPosOverride = newpos;
515 refreshCurrentTime();
519 private void refreshCurrentTimeText(final long pos) {
520 mCurrentTime.setText(MusicUtils.makeTimeString(getActivity(), pos / 1000));
523 /* Used to update the current time string */
524 private long refreshCurrentTime() {
525 if (mService == null) {
529 final long pos = mPosOverride < 0 ? MusicUtils.position() : mPosOverride;
530 if (pos >= 0 && MusicUtils.duration() > 0) {
531 refreshCurrentTimeText(pos);
532 final int progress = (int)(1000 * pos / MusicUtils.duration());
533 mProgress.setProgress(progress);
537 } else if (MusicUtils.isPlaying()) {
538 mCurrentTime.setVisibility(View.VISIBLE);
541 final int vis = mCurrentTime.getVisibility();
542 mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE
547 mCurrentTime.setText("--:--");
548 mProgress.setProgress(1000);
550 // calculate the number of milliseconds until the next full second,
552 // the counter can be updated at just the right time
553 final long remaining = 1000 - pos % 1000;
554 // approximate how often we would need to refresh the slider to
556 int width = mProgress.getWidth();
560 final long smoothrefreshtime = MusicUtils.duration() / width;
561 if (smoothrefreshtime > remaining) {
564 if (smoothrefreshtime < 20) {
567 return smoothrefreshtime;
568 } catch (final Exception ignored) {
575 * /** Used to shared what the user is currently listening to
577 private void shareCurrentTrack() {
578 if (MusicUtils.getTrackName() == null || MusicUtils.getArtistName() == null) {
581 final Intent shareIntent = new Intent();
582 final String shareMessage = getString(R.string.now_listening_to,
583 MusicUtils.getTrackName(), MusicUtils.getArtistName());
585 shareIntent.setAction(Intent.ACTION_SEND);
586 shareIntent.setType("text/plain");
587 shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage);
588 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_track_using)));
592 * Used to scan backwards through the track
594 private final RepeatingImageButton.RepeatListener mRewindListener = new RepeatingImageButton.RepeatListener() {
599 public void onRepeat(final View v, final long howlong, final int repcnt) {
600 scanBackward(repcnt, howlong);
605 * Used to scan ahead through the track
607 private final RepeatingImageButton.RepeatListener mFastForwardListener = new RepeatingImageButton.RepeatListener() {
612 public void onRepeat(final View v, final long howlong, final int repcnt) {
613 scanForward(repcnt, howlong);
618 * Opens to the current album profile
620 private final View.OnClickListener mOpenAlbumProfile = new View.OnClickListener() {
623 public void onClick(final View v) {
624 NavUtils.openAlbumProfile(getActivity(), MusicUtils.getAlbumName(),
625 MusicUtils.getArtistName(), MusicUtils.getCurrentAlbumId());
630 * Used to update the current time string
632 private static final class TimeHandler extends Handler {
634 private final WeakReference<AudioPlayerFragment> mAudioPlayer;
637 * Constructor of <code>TimeHandler</code>
639 public TimeHandler(final AudioPlayerFragment player) {
640 mAudioPlayer = new WeakReference<AudioPlayerFragment>(player);
644 public void handleMessage(final Message msg) {
647 final long next = mAudioPlayer.get().refreshCurrentTime();
648 mAudioPlayer.get().queueNextRefresh(next);
657 * Used to monitor the state of playback
659 private static final class PlaybackStatus extends BroadcastReceiver {
661 private final WeakReference<AudioPlayerFragment> mReference;
664 * Constructor of <code>PlaybackStatus</code>
666 public PlaybackStatus(final AudioPlayerFragment fragment) {
667 mReference = new WeakReference<AudioPlayerFragment>(fragment);
674 public void onReceive(final Context context, final Intent intent) {
675 final String action = intent.getAction();
676 if (action.equals(MusicPlaybackService.META_CHANGED)) {
678 mReference.get().updateNowPlayingInfo();
679 // Update the favorites icon
681 // mReference.get().invalidateOptionsMenu();
682 } else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
683 // Set the play and pause image
684 mReference.get().mPlayPauseButton.updateState();
685 } else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
686 || action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
687 // Set the repeat image
688 mReference.get().mRepeatButton.updateRepeatState();
689 // Set the shuffle image
690 mReference.get().mShuffleButton.updateShuffleState();