content: package allen.town.podcast.playback.cast;
  
  import android.content.Context;
  import androidx.annotation.NonNull;
  import android.util.Log;
  import android.util.Pair;
  import android.view.SurfaceHolder;
  
  import java.util.Collections;
  import java.util.List;
  import java.util.concurrent.Future;
  import java.util.concurrent.FutureTask;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import androidx.annotation.Nullable;
  import com.google.android.gms.cast.MediaError;
  import com.google.android.gms.cast.MediaInfo;
  import com.google.android.gms.cast.MediaLoadOptions;
  import com.google.android.gms.cast.MediaLoadRequestData;
  import com.google.android.gms.cast.MediaSeekOptions;
  import com.google.android.gms.cast.MediaStatus;
  import com.google.android.gms.cast.framework.CastContext;
  import com.google.android.gms.cast.framework.CastState;
  import com.google.android.gms.cast.framework.media.RemoteMediaClient;
  import com.google.android.gms.common.ConnectionResult;
  import com.google.android.gms.common.GoogleApiAvailability;
  import allen.town.podcast.event.PlayerErrorEvent;
  import allen.town.podcast.event.playback.BufferUpdateEvent;
  import allen.town.podcast.model.feed.FeedMedia;
  import allen.town.podcast.model.playback.MediaType;
  import allen.town.podcast.model.playback.Playable;
  import allen.town.podcast.model.playback.RemoteMedia;
  import allen.town.podcast.playback.base.PlaybackServiceMediaPlayer;
  import allen.town.podcast.playback.base.PlayerStatus;
  import allen.town.podcast.playback.base.RewindAfterPauseUtils;
  import org.greenrobot.eventbus.EventBus;
  
  /**
   * for Cast Devices.
   */
  public class CastPsmp extends PlaybackServiceMediaPlayer {
  
      public static final String TAG = "CastPSMP";
  
      private volatile Playable media;
      private volatile MediaType mediaType;
      private volatile MediaInfo remoteMedia;
      private volatile int remoteState;
      private final CastContext castContext;
      private final RemoteMediaClient remoteMediaClient;
  
      private final AtomicBoolean isBuffering;
  
      private final AtomicBoolean startWhenPrepared;
  
      @Nullable
      public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
                                                                      @NonNull PSMPCallback callback) {
          if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
              return null;
          }
          try {
              if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
                  return new CastPsmp(context, callback);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
          return null;
      }
  
      public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
          super(context, callback);
  
          castContext = CastContext.getSharedInstance(context);
          remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
          remoteMediaClient.registerCallback(remoteMediaClientCallback);
          media = null;
          mediaType = null;
          startWhenPrepared = new AtomicBoolean(false);
          isBuffering = new AtomicBoolean(false);
          remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
      }
  
      private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
          @Override
          public void onMetadataUpdated() {
              super.onMetadataUpdated();
              onRemoteMediaPlayerStatusUpdated();
          }
  
          @Override
          public void onPreloadStatusUpdated() {
              super.onPreloadStatusUpdated();
              onRemoteMediaPlayerStatusUpdated();
          }
  
          @Override
          public void onStatusUpdated() {
              super.onStatusUpdated();
              onRemoteMediaPlayerStatusUpdated();
          }
  
          @Override
          public void onMediaError(@NonNull MediaError mediaError) {
              EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
          }
      };
  
      private void setBuffering(boolean buffering) {
          if (buffering && isBuffering.compareAndSet(false, true)) {
              EventBus.getDefault().post(BufferUpdateEvent.started());
          } else if (!buffering && isBuffering.compareAndSet(true, false)) {
              EventBus.getDefault().post(BufferUpdateEvent.ended());
          }
      }
  
      private Playable localVersion(MediaInfo info) {
          if (info == null || info.getMetadata() == null) {
              return null;
          }
          if (CastUtils.matches(info, media)) {
              return media;
          }
          String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
          return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
      }
  
      private MediaInfo remoteVersion(Playable playable) {
          if (playable == null) {
              return null;
          }
          if (CastUtils.matches(remoteMedia, playable)) {
              return remoteMedia;
          }
          if (playable instanceof FeedMedia) {
              return MediaInfoCreator.from((FeedMedia) playable);
          }
          if (playable instanceof RemoteMedia) {
              return MediaInfoCreator.from((RemoteMedia) playable);
          }
          return null;
      }
  
      private void onRemoteMediaPlayerStatusUpdated() {
          MediaStatus status = remoteMediaClient.getMediaStatus();
          if (status == null) {
              Log.d(TAG, "Received null MediaStatus");
              return;
          } else {
              Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState());
          }
          int state = status.getPlayerState();
          int oldState = remoteState;
          remoteMedia = status.getMediaInfo();
          boolean mediaChanged = !CastUtils.matches(remoteMedia, media);
          boolean stateChanged = state != oldState;
          if (!mediaChanged && !stateChanged) {
              Log.d(TAG, "Both media and state haven't changed, so nothing to do");
              return;
          }
          Playable currentMedia = mediaChanged ? localVersion(remoteMedia) : media;
          Playable oldMedia = media;
          int position = (int) status.getStreamPosition();
          // check for incompatible states
          if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED)
                  && currentMedia == null) {
              Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media");
              state = MediaStatus.PLAYER_STATE_UNKNOWN;
              stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN;
          }
  
          if (stateChanged) {
              remoteState = state;
          }
  
          if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
                  && state != MediaStatus.PLAYER_STATE_IDLE) {
              callback.onPlaybackPause(null, INVALID_TIME);
              // We don't want setPlayerStatus to handle the onPlaybackPause callback
              setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
          }
  
          setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING);
  
          switch (state) {
              case MediaStatus.PLAYER_STATE_PLAYING:
                  if (!stateChanged) {
                      //These steps are necessary because they won't be performed by setPlayerStatus()
                      if (position >= 0) {
                          currentMedia.setPosition(position);
                      }
                      currentMedia.onPlaybackStart();
                  }
                  setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position);
                  break;
              case MediaStatus.PLAYER_STATE_PAUSED:
                  setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
                  break;
              case MediaStatus.PLAYER_STATE_BUFFERING:
                  setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
                                  ? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
                          currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
                  break;
              case MediaStatus.PLAYER_STATE_IDLE:
                  int reason = status.getIdleReason();
                  switch (reason) {
                      case MediaStatus.IDLE_REASON_CANCELED:
                          // Essentially means stopped at the request of a user
                          callback.onPlaybackEnded(null, true);
                          setPlayerStatus(PlayerStatus.STOPPED, currentMedia);
                          if (oldMedia != null) {
                              if (position >= 0) {
                                  oldMedia.setPosition(position);
                              }
                              callback.onPostPlayback(oldMedia, false, false, false);
                          }
                          // onPlaybackEnded pretty much takes care of updating the UI
                          return;
                      case MediaStatus.IDLE_REASON_INTERRUPTED:
                          // Means that a request to load a different media was sent
                          // Not sure if currentMedia already reflects the to be loaded one
                          if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) {
                              callback.onPlaybackPause(null, INVALID_TIME);
                              setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
                          }
                          setPlayerStatus(PlayerStatus.PREPARING, currentMedia);
                          break;
                      case MediaStatus.IDLE_REASON_NONE:
                          // This probably only happens when we connected but no command has been sent yet.
                          setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia);
                          break;
                      case MediaStatus.IDLE_REASON_FINISHED:
                          // This is our onCompletionListener...
                          if (mediaChanged && currentMedia != null) {
                              media = currentMedia;
                          }
                          endPlayback(true, false, true, true);
                          return;
                      case MediaStatus.IDLE_REASON_ERROR:
                          Log.w(TAG, "Got an error status from the Chromecast. "
                                  + "Skipping, if possible, to the next episode...");
                          EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
                          endPlayback(false, false, true, true);
                          return;
                      default:
                          return;
                  }
                  break;
              case MediaStatus.PLAYER_STATE_UNKNOWN:
                  if (playerStatus != PlayerStatus.INDETERMINATE || media != currentMedia) {
                      setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
                  }
                  break;
              default:
                  Log.w(TAG, "Remote media state undetermined!");
          }
          if (mediaChanged) {
              callback.onMediaChanged(true);
              if (oldMedia != null) {
                  callback.onPostPlayback(oldMedia, false, false, currentMedia != null);
              }
          }
      }
  
      @Override
      public void playMediaObject(@NonNull final Playable playable, final boolean stream,
                                  final boolean startWhenPrepared, final boolean prepareImmediately) {
          Log.d(TAG, "playMediaObject() called");
          playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
      }
  
      /**
       * 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.
       *
       * @see #playMediaObject(Playable, boolean, boolean, boolean)
       */
      private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
                           final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
          if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
              Log.d(TAG, "media provided is not compatible with cast device");
              EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
              Playable nextPlayable = playable;
              do {
                  nextPlayable = callback.getNextInQueue(nextPlayable);
              } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
                      castContext.getSessionManager().getCurrentCastSession()));
              if (nextPlayable != null) {
                  playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
              }
              return;
          }
  
          if (media != null) {
              if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
                      && playerStatus == PlayerStatus.PLAYING) {
                  // episode is already playing -> ignore method call
                  Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
                  return;
              } else {
                  // set temporarily to pause in order to update list with current position
                  boolean isPlaying = remoteMediaClient.isPlaying();
                  int position = (int) remoteMediaClient.getApproximateStreamPosition();
                  if (isPlaying) {
                      callback.onPlaybackPause(media, position);
                  }
                  if (!media.getIdentifier().equals(playable.getIdentifier())) {
                      final Playable oldMedia = media;
                      callback.onPostPlayback(oldMedia, false, false, true);
                  }
                  setPlayerStatus(PlayerStatus.INDETERMINATE, null);
              }
          }
  
          this.media = playable;
          remoteMedia = remoteVersion(playable);
          this.mediaType = media.getMediaType();
          this.startWhenPrepared.set(startWhenPrepared);
          setPlayerStatus(PlayerStatus.INITIALIZING, media);
          callback.ensureMediaInfoLoaded(media);
          callback.onMediaChanged(true);
          setPlayerStatus(PlayerStatus.INITIALIZED, media);
          if (prepareImmediately) {
              prepare();
          }
      }
  
      @Override
      public void resume() {
          int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
                          media.getPosition(),
                          media.getLastPlayedTime());
          seekTo(newPosition);
          remoteMediaClient.play();
      }
  
      @Override
      public void pause(boolean abandonFocus, boolean reinit) {
          remoteMediaClient.pause();
      }
  
      @Override
      public void prepare() {
          if (playerStatus == PlayerStatus.INITIALIZED) {
              Log.d(TAG, "Preparing media player");
              setPlayerStatus(PlayerStatus.PREPARING, media);
              int position = media.getPosition();
              if (position > 0) {
                  position = RewindAfterPauseUtils.calculatePositionWithRewind(
                          position,
                          media.getLastPlayedTime());
              }
              remoteMediaClient.load(new MediaLoadRequestData.Builder()
                      .setMediaInfo(remoteMedia)
                      .setAutoplay(startWhenPrepared.get())
                      .setCurrentTime(position).build());
          }
      }
  
      @Override
      public void reinit() {
          Log.d(TAG, "reinit() called");
          if (media != null) {
              playMediaObject(media, true, false, startWhenPrepared.get(), false);
          } else {
              Log.d(TAG, "Call to reinit was ignored: media was null");
          }
      }
  
      @Override
      public void seekTo(int t) {
          new Exception("Seeking to " + t).printStackTrace();
          remoteMediaClient.seek(new MediaSeekOptions.Builder()
                  .setPosition(t).build());
      }
  
      @Override
      public void seekDelta(int d) {
          int position = getPosition();
          if (position != INVALID_TIME) {
              seekTo(position + d);
          } else {
              Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
          }
      }
  
      @Override
      public int getDuration() {
          int retVal = (int) remoteMediaClient.getStreamDuration();
          if (retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
              retVal = media.getDuration();
          }
          return retVal;
      }
  
      @Override
      public int getPosition() {
          int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
          if (retVal <= 0 && media != null && media.getPosition() >= 0) {
              retVal = media.getPosition();
          }
          return retVal;
      }
  
      @Override
      public boolean isStartWhenPrepared() {
          return startWhenPrepared.get();
      }
  
      @Override
      public void setStartWhenPrepared(boolean startWhenPrepared) {
          this.startWhenPrepared.set(startWhenPrepared);
      }
  
      @Override
      public void setPlaybackParams(float speed, boolean skipSilence) {
          double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
                  Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
          remoteMediaClient.setPlaybackRate(playbackRate);
      }
  
      @Override
      public float getPlaybackSpeed() {
          MediaStatus status = remoteMediaClient.getMediaStatus();
          return status != null ? (float) status.getPlaybackRate() : 1.0f;
      }
  
      @Override
      public void setVolume(float volumeLeft, float volumeRight) {
          Log.d(TAG, "Setting the Stream volume on Remote Media Player");
          remoteMediaClient.setStreamVolume(volumeLeft);
      }
  
      @Override
      public boolean canDownmix() {
          return false;
      }
  
      @Override
      public void setDownmix(boolean enable) {
          throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player");
      }
  
      @Override
      public void setLoudness(boolean enable) {
          throw new UnsupportedOperationException("Setting loudness unsupported in Remote Media Player");
      }
  
      @Override
      public MediaType getCurrentMediaType() {
          return mediaType;
      }
  
      @Override
      public boolean isStreaming() {
          return true;
      }
  
      @Override
      public void shutdown() {
          remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
      }
  
      @Override
      public void setVideoSurface(SurfaceHolder surface) {
          throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
      }
  
      @Override
      public void resetVideoSurface() {
          Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player");
      }
  
      @Override
      public Pair<Integer, Integer> getVideoSize() {
          return null;
      }
  
      @Override
      public Playable getPlayable() {
          return media;
      }
  
      @Override
      protected void setPlayable(Playable playable) {
          if (playable != media) {
              media = playable;
              remoteMedia = remoteVersion(playable);
          }
      }
  
      @Override
      public List<String> getAudioTracks() {
          return Collections.emptyList();
      }
  
      public void setAudioTrack(int track) {
  
      }
  
      public int getSelectedAudioTrack() {
          return -1;
      }
  
      @Override
      protected Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue,
                                      boolean toStoppedState) {
          Log.d(TAG, "endPlayback() called");
          boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
          if (playerStatus != PlayerStatus.INDETERMINATE) {
              setPlayerStatus(PlayerStatus.INDETERMINATE, media);
          }
          if (media != null && wasSkipped) {
              // current position only really matters when we skip
              int position = getPosition();
              if (position >= 0) {
                  media.setPosition(position);
              }
          }
          final Playable currentMedia = media;
          Playable nextMedia = null;
          if (shouldContinue) {
              nextMedia = callback.getNextInQueue(currentMedia);
  
              boolean playNextEpisode = isPlaying && nextMedia != null;
              if (playNextEpisode) {
                  Log.d(TAG, "Playback of next episode will start immediately.");
              } else if (nextMedia == null) {
                  Log.d(TAG, "No more episodes available to play");
              } else {
                  Log.d(TAG, "Loading next episode, but not playing automatically.");
              }
  
              if (nextMedia != null) {
                  callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
                  // setting media to null signals to playMediaObject() that we're taking care of post-playback processing
                  media = null;
                  playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
              }
          }
          if (shouldContinue || toStoppedState) {
              if (nextMedia == null) {
                  remoteMediaClient.stop();
                  // Otherwise we rely on the chromecast callback to tell us the playback has stopped.
                  callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
              } else {
                  callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
              }
          } else if (isPlaying) {
              callback.onPlaybackPause(currentMedia,
                      currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
          }
  
          FutureTask<?> future = new FutureTask<>(() -> { }, null);
          future.run();
          return future;
      }
  
      @Override
      protected boolean shouldLockWifi() {
          return false;
      }
  
      @Override
      public boolean isCasting() {
          return true;
      }
  }
  