public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
    Validate.notNull(context);
    Validate.notNull(callback);

    this.context = context;
    this.callback = callback;
    this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    this.playerLock = new ReentrantLock();
    this.startWhenPrepared = new AtomicBoolean(false);
    executor =
        new ThreadPoolExecutor(
            1,
            1,
            5,
            TimeUnit.MINUTES,
            new LinkedBlockingDeque<Runnable>(),
            new RejectedExecutionHandler() {
              @Override
              public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable");
              }
            });

    mediaSession = new MediaSessionCompat(context, TAG);
    mediaSession.setCallback(sessionCallback);
    mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

    mediaPlayer = null;
    statusBeforeSeeking = null;
    pausedBecauseOfTransientAudiofocusLoss = false;
    mediaType = MediaType.UNKNOWN;
    playerStatus = PlayerStatus.STOPPED;
    videoSize = null;
  }
  /**
   * Update the current media player state, optionally showing an error message.
   *
   * @param error if not null, error message to present to the user.
   */
  private void updatePlaybackState(String error) {
    LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
    long position = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN;
    if (mPlayback != null && mPlayback.isConnected()) {
      position = mPlayback.getCurrentStreamPosition();
    }

    PlaybackStateCompat.Builder stateBuilder =
        new PlaybackStateCompat.Builder().setActions(getAvailableActions());

    setCustomAction(stateBuilder);
    int state = mPlayback.getState();

    // If there is an error message, send it to the playback state:
    if (error != null) {
      // Error states are really only supposed to be used for errors that cause playback to
      // stop unexpectedly and persist until the user takes action to fix it.
      stateBuilder.setErrorMessage(error);
      state = PlaybackStateCompat.STATE_ERROR;
    }
    stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());

    // Set the activeQueueItemId if the current index is valid.
    if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
      MediaSessionCompat.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
      stateBuilder.setActiveQueueItemId(item.getQueueId());
    }

    mSession.setPlaybackState(stateBuilder.build());

    if (state == PlaybackStateCompat.STATE_PLAYING || state == PlaybackStateCompat.STATE_PAUSED) {
      mMediaNotificationManager.startNotification();
    }
  }
 @Override
 public void onDisconnected() {
   LogHelper.d(TAG, "onDisconnected");
   mSessionExtras.remove(EXTRA_CONNECTED_CAST);
   mSession.setExtras(mSessionExtras);
   Playback playback = new LocalPlayback(MusicService.this, mMusicProvider);
   mMediaRouter.setMediaSession(null);
   switchToPlayer(playback, false);
 }
  /**
   * Build a new notification. To update the progress on the notification, use {@link
   * #updateProgress(int, int)} instead.
   *
   * @param episode The episode playing.
   * @param paused Playback state, <code>true</code> for paused.
   * @param canSeek If the currently played media is seekable.
   * @param position The current playback progress.
   * @param duration The length of the current episode.
   * @param session The media session representing current playback.
   * @return The notification to display.
   */
  @NonNull
  public Notification build(
      Episode episode,
      boolean paused,
      boolean canSeek,
      int position,
      int duration,
      MediaSessionCompat session) {
    // 0. Prepare the main intent (leading back to the app)
    appIntent.putExtra(PODCAST_URL_KEY, episode.getPodcast().getUrl());
    appIntent.putExtra(EPISODE_URL_KEY, episode.getMediaUrl());
    final PendingIntent backToAppIntent =
        PendingIntent.getActivity(context, 0, appIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    // 1. Create the notification builder and set values
    notificationBuilder = new NotificationCompat.Builder(context);
    notificationBuilder
        .setContentIntent(backToAppIntent)
        .setTicker(episode.getName())
        .setSmallIcon(R.drawable.ic_stat)
        .setContentTitle(episode.getName())
        .setContentText(episode.getPodcast().getName())
        .setWhen(0)
        .setProgress(duration, position, false)
        .setOngoing(true);

    // 2. Load large image if available, see onBitmapLoaded() below
    if (episode.getPodcast().hasLogoUrl())
      Picasso.with(context)
          .load(episode.getPodcast().getLogoUrl())
          .resizeDimen(
              android.R.dimen.notification_large_icon_width,
              android.R.dimen.notification_large_icon_height)
          .into(this);

    // 3. Add actions to notification
    notificationBuilder.addAction(stopAction);
    if (canSeek) notificationBuilder.addAction(rewindAction);
    if (paused) notificationBuilder.addAction(playAction);
    else notificationBuilder.addAction(pauseAction);
    if (canSeek) notificationBuilder.addAction(forwardAction);

    // 4. Apply other notification features
    NotificationCompat.MediaStyle style =
        new NotificationCompat.MediaStyle().setMediaSession(session.getSessionToken());
    // Make sure not to show rew/ff icons for live streams
    if (canSeek) style.setShowActionsInCompactView(1, 2, 3); // rewind, toggle play, forward
    else style.setShowActionsInCompactView(0, 1); // stop, toggle play

    notificationBuilder.setStyle(style);
    notificationBuilder.setColor(ContextCompat.getColor(context, R.color.theme_dark));
    notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
    notificationBuilder.setCategory(NotificationCompat.CATEGORY_TRANSPORT);

    return notificationBuilder.build();
  }
 /**
  * Releases internally used resources. This method should only be called when the object is not
  * used anymore.
  */
 public void shutdown() {
   executor.shutdown();
   if (mediaPlayer != null) {
     mediaPlayer.release();
   }
   if (mediaSession != null) {
     mediaSession.release();
   }
   releaseWifiLockIfNecessary();
 }
 /**
  * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON} intent,
  * passing it onto the {@link MediaSessionCompat} using {@link
  * MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn will trigger callbacks
  * to the {@link MediaSessionCompat.Callback} registered via {@link
  * MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}.
  *
  * <p>The returned {@link KeyEvent} is non-null if any {@link KeyEvent} is found and can be used
  * if any additional processing is needed beyond what is done in the {@link
  * MediaSessionCompat.Callback}. An example of is to prevent redelivery of a {@link
  * KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} Intent in the case of the Service being restarted (which, by
  * default, will redeliver the last received Intent).
  *
  * <pre>
  * KeyEvent keyEvent = MediaButtonReceiver.handleIntent(mediaSession, intent);
  * if (keyEvent != null && keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
  *   Intent emptyIntent = new Intent(intent);
  *   emptyIntent.setAction("");
  *   startService(emptyIntent);
  * }
  * </pre>
  *
  * @param mediaSessionCompat A {@link MediaSessionCompat} that has a {@link
  *     MediaSessionCompat.Callback} set.
  * @param intent The intent to parse.
  * @return The extracted {@link KeyEvent} if found, or null.
  */
 public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
   if (mediaSessionCompat == null
       || intent == null
       || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
       || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
     return null;
   }
   KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
   MediaControllerCompat mediaController = mediaSessionCompat.getController();
   mediaController.dispatchMediaButtonEvent(ke);
   return ke;
 }
 @Override
 public void onApplicationConnected(
     ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
   // In case we are casting, send the device name as an extra on MediaSessionCompat
   // metadata.
   mSessionExtras.putString(EXTRA_CONNECTED_CAST, mCastManager.getDeviceName());
   mSession.setExtras(mSessionExtras);
   // Now we can switch to CastPlayback
   Playback playback = new CastPlayback(mMusicProvider);
   mMediaRouter.setMediaSession(mSession);
   switchToPlayer(playback, true);
 }
  /** Handle a request to play music */
  private void handlePlayRequest() {
    LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());

    mDelayedStopHandler.removeCallbacksAndMessages(null);
    if (!mServiceStarted) {
      LogHelper.v(TAG, "Starting service");
      // The MusicService needs to keep running even after the calling MediaBrowserCompat
      // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
      // need to play media.
      startService(new Intent(getApplicationContext(), MusicService.class));
      mServiceStarted = true;
    }

    if (!mSession.isActive()) {
      mSession.setActive(true);
    }

    if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
      updateMetadata();
      mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
    }
  }
  /**
   * (non-Javadoc)
   *
   * @see android.app.Service#onDestroy()
   */
  @Override
  public void onDestroy() {
    LogHelper.d(TAG, "onDestroy");
    unregisterReceiver(mCarConnectionReceiver);
    // Service is being killed, so make sure we release our resources
    handleStopRequest(null);

    mCastManager = VideoCastManager.getInstance();
    mCastManager.removeVideoCastConsumer(mCastConsumer);

    mDelayedStopHandler.removeCallbacksAndMessages(null);
    // Always release the MediaSessionCompat to clean up resources
    // and notify associated MediaControllerCompat(s).
    mSession.release();
  }
 /**
  * Returns a token to this object's MediaSession. The MediaSession should only be used for
  * notifications at the moment.
  *
  * @return The MediaSessionCompat.Token object.
  */
 public MediaSessionCompat.Token getSessionToken() {
   return mediaSession.getSessionToken();
 }
  /**
   * Internal implementation of playMediaObject. This method has an additional parameter that allows
   * the caller to force a media player reset even if the given playable parameter is the same
   * object as the currently playing media.
   *
   * <p>This method requires the playerLock and is executed on the caller's thread.
   *
   * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean,
   *     boolean)
   */
  private void playMediaObject(
      final Playable playable,
      final boolean forceReset,
      final boolean stream,
      final boolean startWhenPrepared,
      final boolean prepareImmediately) {
    Validate.notNull(playable);
    if (!playerLock.isHeldByCurrentThread())
      throw new IllegalStateException("method requires playerLock");

    if (media != null) {
      if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) {
        // episode is already playing -> ignore method call
        if (BuildConfig.DEBUG)
          Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
        return;
      } else {
        // stop playback of this episode
        if (playerStatus == PlayerStatus.PAUSED
            || playerStatus == PlayerStatus.PLAYING
            || playerStatus == PlayerStatus.PREPARED) {
          mediaPlayer.stop();
        }
        setPlayerStatus(PlayerStatus.INDETERMINATE, null);
      }
    }

    this.media = playable;
    this.stream = stream;
    this.mediaType = media.getMediaType();
    this.videoSize = null;
    createMediaPlayer();
    PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
    setPlayerStatus(PlayerStatus.INITIALIZING, media);
    try {
      media.loadMetadata();
      mediaSession.setMetadata(getMediaSessionMetadata(media));
      if (stream) {
        mediaPlayer.setDataSource(media.getStreamUrl());
      } else {
        mediaPlayer.setDataSource(media.getLocalMediaUrl());
      }
      setPlayerStatus(PlayerStatus.INITIALIZED, media);

      if (mediaType == MediaType.VIDEO) {
        VideoPlayer vp = (VideoPlayer) mediaPlayer;
        //  vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
      }

      if (prepareImmediately) {
        setPlayerStatus(PlayerStatus.PREPARING, media);
        mediaPlayer.prepare();
        onPrepared(startWhenPrepared);
      }

    } catch (Playable.PlayableException e) {
      e.printStackTrace();
      setPlayerStatus(PlayerStatus.ERROR, null);
    } catch (IOException e) {
      e.printStackTrace();
      setPlayerStatus(PlayerStatus.ERROR, null);
    } catch (IllegalStateException e) {
      e.printStackTrace();
      setPlayerStatus(PlayerStatus.ERROR, null);
    }
  }
  private void updateMetadata() {
    if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
      LogHelper.e(TAG, "Can't retrieve current metadata.");
      updatePlaybackState(getResources().getString(R.string.error_no_metadata));
      return;
    }
    MediaSessionCompat.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
    String musicId =
        MediaIDHelper.extractMusicIDFromMediaID(queueItem.getDescription().getMediaId());
    MediaMetadataCompat track = mMusicProvider.getMusic(musicId);
    if (track == null) {
      throw new IllegalArgumentException("Invalid musicId " + musicId);
    }
    final String trackId = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
    if (!TextUtils.equals(musicId, trackId)) {
      IllegalStateException e = new IllegalStateException("track ID should match musicId.");
      LogHelper.e(
          TAG,
          "track ID should match musicId.",
          " musicId=",
          musicId,
          " trackId=",
          trackId,
          " mediaId from queueItem=",
          queueItem.getDescription().getMediaId(),
          " title from queueItem=",
          queueItem.getDescription().getTitle(),
          " mediaId from track=",
          track.getDescription().getMediaId(),
          " title from track=",
          track.getDescription().getTitle(),
          " source.hashcode from track=",
          track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(),
          e);
      throw e;
    }
    LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
    mSession.setMetadata(track);

    // Set the proper album artwork on the media session, so it can be shown in the
    // locked screen and in other places.
    if (track.getDescription().getIconBitmap() == null
        && track.getDescription().getIconUri() != null) {
      String albumUri = track.getDescription().getIconUri().toString();
      AlbumArtCache.getInstance()
          .fetch(
              albumUri,
              new AlbumArtCache.FetchListener() {
                @Override
                public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
                  MediaSessionCompat.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
                  MediaMetadataCompat track = mMusicProvider.getMusic(trackId);
                  track =
                      new MediaMetadataCompat.Builder(track)

                          // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for
                          // example, on the lockscreen background when the media session is active.
                          .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)

                          // set small version of the album art in the DISPLAY_ICON. This is used on
                          // the MediaDescriptionCompat and thus it should be small to be serialized
                          // if
                          // necessary..
                          .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, icon)
                          .build();

                  mMusicProvider.updateMusic(trackId, track);

                  // If we are still playing the same music
                  String currentPlayingId =
                      MediaIDHelper.extractMusicIDFromMediaID(
                          queueItem.getDescription().getMediaId());
                  if (trackId.equals(currentPlayingId)) {
                    mSession.setMetadata(track);
                  }
                }
              });
    }
  }
  /*
   * (non-Javadoc)
   * @see android.app.Service#onCreate()
   */
  @Override
  public void onCreate() {
    super.onCreate();
    LogHelper.d(TAG, "onCreate");

    mPlayingQueue = new ArrayList<>();
    mMusicProvider = new MusicProvider();
    mPackageValidator = new PackageValidator(this);

    ComponentName mediaButtonReceiver = new ComponentName(this, RemoteControlReceiver.class);

    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    mediaButtonIntent.setComponent(mediaButtonReceiver);
    PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);

    // Start a new MediaSessionCompat
    mSession =
        new MediaSessionCompat(this, "MusicService", mediaButtonReceiver, mediaPendingIntent);

    /*
    final MediaSessionCallback cb = new MediaSessionCallback();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // Shouldn't really have to do this but the MediaSessionCompat method uses
        // an internal proxy class, which doesn't forward events such as
        // onPlayFromMediaId when running on Lollipop.
        final MediaSession session = (MediaSession) mSession.getMediaSession();
        session.setCallback(new MediaSessionCallbackProxy(cb));
    } else {
        mSession.setCallback(cb);
    }
    */

    setSessionToken(mSession.getSessionToken());
    mSession.setCallback(new MediaSessionCallback());
    mSession.setFlags(
        MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
            | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

    mPlayback = new LocalPlayback(this, mMusicProvider);
    mPlayback.setState(PlaybackStateCompat.STATE_NONE);
    mPlayback.setCallback(this);
    mPlayback.start();

    Context context = getApplicationContext();
    Intent intent = new Intent(context, NowPlayingActivity.class);
    PendingIntent pi =
        PendingIntent.getActivity(
            context, 99 /*request code*/, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    mSession.setSessionActivity(pi);

    mSessionExtras = new Bundle();
    CarHelper.setSlotReservationFlags(mSessionExtras, true, true, true);
    WearHelper.setSlotReservationFlags(mSessionExtras, true, true);
    WearHelper.setUseBackgroundFromTheme(mSessionExtras, true);
    mSession.setExtras(mSessionExtras);

    updatePlaybackState(null);

    mMediaNotificationManager = new MediaNotificationManager(this);
    mCastManager = VideoCastManager.getInstance();
    mCastManager.addVideoCastConsumer(mCastConsumer);
    mMediaRouter = MediaRouter.getInstance(getApplicationContext());

    IntentFilter filter = new IntentFilter(CarHelper.ACTION_MEDIA_STATUS);
    mCarConnectionReceiver =
        new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
            String connectionEvent = intent.getStringExtra(CarHelper.MEDIA_CONNECTION_STATUS);
            mIsConnectedToCar = CarHelper.MEDIA_CONNECTED.equals(connectionEvent);
            LogHelper.i(
                TAG,
                "Connection event to Android Auto: ",
                connectionEvent,
                " isConnectedToCar=",
                mIsConnectedToCar);
          }
        };
    registerReceiver(mCarConnectionReceiver, filter);
  }