@@ -872,4 +872,17 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
             }
         }
     }
+
+    protected float getPlaybackSpeedForMedia() {
+        if (controller != null) {
+            Playable media = controller.getMedia();
+            boolean isFeedMedia = media instanceof FeedMedia;
+
+            if (isFeedMedia) {
+                return ((FeedMedia) media).getMediaPlaybackSpeed();
+            }
+        }
+
+        return UserPreferences.getPlaybackSpeed();
+    }
 }
package de.danoeh.antennapod.activity;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;

import com.afollestad.materialdialogs.MaterialDialog;
import com.bumptech.glide.Glide;
import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.FontAwesomeIcons;

import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Consumer;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.Flavors;
import de.danoeh.antennapod.core.util.Function;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.Supplier;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;


/**
 * Provides general features which are both needed for playing audio and video
 * files.
 */
public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener {
    private static final String TAG = "MediaplayerActivity";
    private static final String PREFS = "MediaPlayerActivityPreferences";
    private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
    private static final int REQUEST_CODE_STORAGE = 42;

    PlaybackController controller;

    private TextView txtvPosition;
    private TextView txtvLength;
    SeekBar sbPosition;
    private ImageButton butRev;
    private TextView txtvRev;
    private ImageButton butPlay;
    private ImageButton butFF;
    private TextView txtvFF;
    private ImageButton butSkip;

    private boolean showTimeLeft = false;

    private boolean isFavorite = false;

    private Disposable disposable;

    private PlaybackController newPlaybackController() {
        return new PlaybackController(this, false) {

            @Override
            public void setupGUI() {
                MediaplayerActivity.this.setupGUI();
            }

            @Override
            public void onPositionObserverUpdate() {
                MediaplayerActivity.this.onPositionObserverUpdate();
            }

            @Override
            public void onBufferStart() {
                MediaplayerActivity.this.onBufferStart();
            }

            @Override
            public void onBufferEnd() {
                MediaplayerActivity.this.onBufferEnd();
            }

            @Override
            public void onBufferUpdate(float progress) {
                MediaplayerActivity.this.onBufferUpdate(progress);
            }

            @Override
            public void handleError(int code) {
                MediaplayerActivity.this.handleError(code);
            }

            @Override
            public void onReloadNotification(int code) {
                MediaplayerActivity.this.onReloadNotification(code);
            }

            @Override
            public void onSleepTimerUpdate() {
                supportInvalidateOptionsMenu();
            }

            @Override
            public ImageButton getPlayButton() {
                return butPlay;
            }

            @Override
            public void postStatusMsg(int msg, boolean showToast) {
                MediaplayerActivity.this.postStatusMsg(msg, showToast);
            }

            @Override
            public void clearStatusMsg() {
                MediaplayerActivity.this.clearStatusMsg();
            }

            @Override
            public boolean loadMediaInfo() {
                return MediaplayerActivity.this.loadMediaInfo();
            }

            @Override
            public void onAwaitingVideoSurface() {
                MediaplayerActivity.this.onAwaitingVideoSurface();
            }

            @Override
            public void onServiceQueried() {
                MediaplayerActivity.this.onServiceQueried();
            }

            @Override
            public void onShutdownNotification() {
                finish();
            }

            @Override
            public void onPlaybackEnd() {
                finish();
            }

            @Override
            public void onPlaybackSpeedChange() {
                MediaplayerActivity.this.onPlaybackSpeedChange();
            }

            @Override
            protected void setScreenOn(boolean enable) {
                super.setScreenOn(enable);
                MediaplayerActivity.this.setScreenOn(enable);
            }

            @Override
            public void onSetSpeedAbilityChanged() {
                MediaplayerActivity.this.onSetSpeedAbilityChanged();
            }
        };
    }

    private static TextView getTxtvFFFromActivity(MediaplayerActivity activity) {
        return activity.txtvFF;
    }
    private static TextView getTxtvRevFromActivity(MediaplayerActivity activity) {
        return activity.txtvRev;
    }

    private void onSetSpeedAbilityChanged() {
        Log.d(TAG, "onSetSpeedAbilityChanged()");
        updatePlaybackSpeedButton();
    }

    private void onPlaybackSpeedChange() {
        updatePlaybackSpeedButtonText();
    }

    private void onServiceQueried() {
        supportInvalidateOptionsMenu();
    }

    void chooseTheme() {
        setTheme(UserPreferences.getTheme());
    }

    void setScreenOn(boolean enable) {
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        chooseTheme();
        super.onCreate(savedInstanceState);

        Log.d(TAG, "onCreate()");
        StorageUtils.checkStorageAvailability(this);

        getWindow().setFormat(PixelFormat.TRANSPARENT);
        setupGUI();
    }

    @Override
    protected void onPause() {
        if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
            if (controller != null) {
                controller.reinitServiceIfPaused();
                controller.pause();
            }
        }
        super.onPause();
    }

    /**
     * Should be used to switch to another player activity if the mime type is
     * not the correct one for the current activity.
     */
    protected abstract void onReloadNotification(int notificationCode);

    /**
     * Should be used to inform the user that the PlaybackService is currently
     * buffering.
     */
    protected abstract void onBufferStart();

    /**
     * Should be used to hide the view that was showing the 'buffering'-message.
     */
    protected abstract void onBufferEnd();

    private void onBufferUpdate(float progress) {
        if (sbPosition != null) {
            sbPosition.setSecondaryProgress((int) (progress * sbPosition.getMax()));
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        controller = newPlaybackController();
        controller.init();
        loadMediaInfo();
        onPositionObserverUpdate();
    }

    @Override
    protected void onStop() {
        Log.d(TAG, "onStop()");
        if (controller != null) {
            controller.release();
            controller = null; // prevent leak
        }
        if (disposable != null) {
            disposable.dispose();
        }
        super.onStop();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        Glide.get(this).trimMemory(level);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        Glide.get(this).clearMemory();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        if (Flavors.FLAVOR == Flavors.PLAY) {
            requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
        }
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.mediaplayer, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        if (controller == null) {
            return false;
        }
        Playable media = controller.getMedia();
        boolean isFeedMedia = media != null && (media instanceof FeedMedia);

        boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null );
        menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink);

        boolean isItemAndHasLink = isFeedMedia &&
                ShareUtils.hasLinkToShare(((FeedMedia) media).getItem());
        menu.findItem(R.id.share_link_item).setVisible(isItemAndHasLink);
        menu.findItem(R.id.share_link_with_position_item).setVisible(isItemAndHasLink);

        boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownload_url() != null;
        menu.findItem(R.id.share_download_url_item).setVisible(isItemHasDownloadLink);
        menu.findItem(R.id.share_download_url_with_position_item).setVisible(isItemHasDownloadLink);
        menu.findItem(R.id.share_file).setVisible(isFeedMedia && ((FeedMedia) media).fileExists());

        menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink);

        menu.findItem(R.id.add_to_favorites_item).setVisible(false);
        menu.findItem(R.id.remove_from_favorites_item).setVisible(false);
        if (isFeedMedia) {
            menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite);
            menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite);
        }

        boolean sleepTimerSet = controller.sleepTimerActive();
        boolean sleepTimerNotSet = controller.sleepTimerNotActive();
        menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet);
        menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet);

        if (this instanceof AudioplayerActivity) {
            int[] attrs = {R.attr.action_bar_icon_color};
            TypedArray ta = obtainStyledAttributes(UserPreferences.getTheme(), attrs);
            int textColor = ta.getColor(0, Color.GRAY);
            ta.recycle();
            menu.findItem(R.id.audio_controls).setIcon(new IconDrawable(this,
                    FontAwesomeIcons.fa_sliders).color(textColor).actionBarSize());
        } else {
            menu.findItem(R.id.audio_controls).setIcon(new IconDrawable(this,
                    FontAwesomeIcons.fa_sliders).color(0xffffffff).actionBarSize());
        }

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (controller == null) {
            return false;
        }
        Playable media = controller.getMedia();
        if (item.getItemId() == android.R.id.home) {
            Intent intent = new Intent(MediaplayerActivity.this,
                    MainActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                    | Intent.FLAG_ACTIVITY_NEW_TASK);

            View cover = findViewById(R.id.imgvCover);
            if (cover != null && Build.VERSION.SDK_INT >= 16) {
                ActivityOptionsCompat options = ActivityOptionsCompat.
                        makeSceneTransitionAnimation(MediaplayerActivity.this,
                        cover, "coverTransition");
                startActivity(intent, options.toBundle());
            } else {
                startActivity(intent);
            }
            finish();
            return true;
        } else {
            if (media != null) {
                switch (item.getItemId()) {
                    case R.id.add_to_favorites_item:
                        if(media instanceof FeedMedia) {
                            FeedItem feedItem = ((FeedMedia)media).getItem();
                            if(feedItem != null) {
                                DBWriter.addFavoriteItem(feedItem);
                                isFavorite = true;
                                invalidateOptionsMenu();
                                Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT)
                                     .show();
                            }
                        }
                        break;
                    case R.id.remove_from_favorites_item:
                        if(media instanceof FeedMedia) {
                            FeedItem feedItem = ((FeedMedia)media).getItem();
                            if(feedItem != null) {
                                DBWriter.removeFavoriteItem(feedItem);
                                isFavorite = false;
                                invalidateOptionsMenu();
                                Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT)
                                     .show();
                            }
                        }
                        break;
                    case R.id.disable_sleeptimer_item:
                        if (controller.serviceAvailable()) {

                            MaterialDialog.Builder stDialog = new MaterialDialog.Builder(this);
                            stDialog.title(R.string.sleep_timer_label);
                            stDialog.content(getString(R.string.time_left_label)
                                    + Converter.getDurationStringLong((int) controller
                                    .getSleepTimerTimeLeft()));
                            stDialog.positiveText(R.string.disable_sleeptimer_label);
                            stDialog.negativeText(R.string.cancel_label);
                            stDialog.onPositive((dialog, which) -> {
                                dialog.dismiss();
                                controller.disableSleepTimer();
                            });
                            stDialog.onNegative((dialog, which) -> dialog.dismiss());
                            stDialog.build().show();
                        }
                        break;
                    case R.id.set_sleeptimer_item:
                        if (controller.serviceAvailable()) {
                            SleepTimerDialog td = new SleepTimerDialog(this) {
                                @Override
                                public void onTimerSet(long millis, boolean shakeToReset, boolean vibrate) {
                                    controller.setSleepTimer(millis, shakeToReset, vibrate);
                                }
                            };
                            td.createNewDialog().show();
                        }
                        break;
                    case R.id.audio_controls:
                        boolean isPlayingVideo = controller.getMedia().getMediaType() == MediaType.VIDEO;
                        PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(isPlayingVideo);
                        dialog.show(getSupportFragmentManager(), "playback_controls");
                        break;
                    case R.id.visit_website_item:
                        Uri uri = Uri.parse(getWebsiteLinkWithFallback(media));
                        startActivity(new Intent(Intent.ACTION_VIEW, uri));
                        break;
                    case R.id.share_link_item:
                        if (media instanceof FeedMedia) {
                            ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem());
                        }
                        break;
                    case R.id.share_download_url_item:
                        if (media instanceof FeedMedia) {
                            ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem());
                        }
                        break;
                    case R.id.share_link_with_position_item:
                        if (media instanceof FeedMedia) {
                            ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem(), true);
                        }
                        break;
                    case R.id.share_download_url_with_position_item:
                        if (media instanceof FeedMedia) {
                            ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem(), true);
                        }
                        break;
                    case R.id.share_file:
                        if (media instanceof FeedMedia) {
                            ShareUtils.shareFeedItemFile(this, ((FeedMedia) media));
                        }
                        break;
                    default:
                        return false;
                }
                return true;
            } else {
                return false;
            }
        }
    }

    private static String getWebsiteLinkWithFallback(Playable media) {
        if (media == null) {
            return null;
        } else if (media.getWebsiteLink() != null) {
            return media.getWebsiteLink();
        } else if (media instanceof FeedMedia) {
            return FeedItemUtil.getLinkWithFallback(((FeedMedia)media).getItem());
        }
        return null;
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume()");
        StorageUtils.checkStorageAvailability(this);
    }

    /**
     * Called by 'handleStatus()' when the PlaybackService is waiting for
     * a video surface.
     */
    protected abstract void onAwaitingVideoSurface();

    protected abstract void postStatusMsg(int resId, boolean showToast);

    protected abstract void clearStatusMsg();

    void onPositionObserverUpdate() {
        if (controller == null || txtvPosition == null || txtvLength == null) {
            return;
        }

        TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
        int currentPosition = converter.convert(controller.getPosition());
        int duration = converter.convert(controller.getDuration());
        int remainingTime = converter.convert(
                controller.getDuration() - controller.getPosition());
        Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
        if (currentPosition == PlaybackService.INVALID_TIME ||
                duration == PlaybackService.INVALID_TIME) {
            Log.w(TAG, "Could not react to position observer update because of invalid time");
            return;
        }
        txtvPosition.setText(Converter.getDurationStringLong(currentPosition));
        if (showTimeLeft) {
            txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime));
        } else {
            txtvLength.setText(Converter.getDurationStringLong(duration));
        }
        updateProgressbarPosition(currentPosition, duration);
    }

    private void updateProgressbarPosition(int position, int duration) {
        Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")");
        if(sbPosition == null) {
            return;
        }
        float progress = ((float) position) / duration;
        sbPosition.setProgress((int) (progress * sbPosition.getMax()));
    }

    /**
     * Load information about the media that is going to be played or currently
     * being played. This method will be called when the activity is connected
     * to the PlaybackService to ensure that the activity has the right
     * FeedMedia object.
     */
    boolean loadMediaInfo() {
        Log.d(TAG, "loadMediaInfo()");
        if(controller == null || controller.getMedia() == null) {
            return false;
        }
        SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
        showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
        onPositionObserverUpdate();
        checkFavorite();
        updatePlaybackSpeedButton();
        return true;
    }

    void updatePlaybackSpeedButton() {
        // Only meaningful on AudioplayerActivity, where it is overridden.
    }

    void updatePlaybackSpeedButtonText() {
        // Only meaningful on AudioplayerActivity, where it is overridden.
    }

    /**
     * Abstract directions to skip forward or back (rewind) and encapsulates behavior to get or set preference (including update of UI on the skip buttons).
     */
    public enum SkipDirection {
        SKIP_FORWARD(
                UserPreferences::getFastForwardSecs,
                MediaplayerActivity::getTxtvFFFromActivity,
                UserPreferences::setFastForwardSecs,
                R.string.pref_fast_forward),
        SKIP_REWIND(UserPreferences::getRewindSecs,
                MediaplayerActivity::getTxtvRevFromActivity,
                UserPreferences::setRewindSecs,
                R.string.pref_rewind);

        private final Supplier<Integer> getPrefSecsFn;
        private final Function<MediaplayerActivity, TextView> getTextViewFn;
        private final Consumer<Integer> setPrefSecsFn;
        private final int titleResourceID;

        /**
         *  Constructor for skip direction enum.  Stores references to  utility functions and resource
         *  id's that vary dependending on the direction.
         *
         * @param getPrefSecsFn Handle to function that retrieves current seconds of the skip delta
         * @param getTextViewFn Handle to function that gets the TextView which displays the current skip delta value
         * @param setPrefSecsFn Handle to function that sets the preference (setting) for the skip delta value (and optionally updates the button label with the current values)
         * @param titleResourceID ID of the resource string with the title for a view
         */
        SkipDirection(Supplier<Integer> getPrefSecsFn, Function<MediaplayerActivity, TextView> getTextViewFn, Consumer<Integer> setPrefSecsFn, int titleResourceID) {
            this.getPrefSecsFn = getPrefSecsFn;
            this.getTextViewFn = getTextViewFn;
            this.setPrefSecsFn = setPrefSecsFn;
            this.titleResourceID = titleResourceID;
        }


        public int getPrefSkipSeconds() {
            return(getPrefSecsFn.get());
        }

        /**
         * Updates preferences for a forward or backward skip depending on the direction of the instance, optionally updating the UI.
         *
         * @param seconds Number of seconds to set the preference associated with the direction of the instance.
         * @param activity MediaplyerActivity that contains textview to update the display of the skip delta setting (or null if nothing to update)
         */
        public void setPrefSkipSeconds(int seconds, @Nullable Activity activity) {
            setPrefSecsFn.accept(seconds);

            if (activity != null && activity instanceof  MediaplayerActivity)  {
                TextView tv = getTextViewFn.apply((MediaplayerActivity)activity);
                if (tv != null) tv.setText(String.valueOf(seconds));
            }
        }
        public int getTitleResourceID() {
            return titleResourceID;
        }
    }

    static public void showSkipPreference(Activity activity, SkipDirection direction) {
        int checked = 0;
        int skipSecs = direction.getPrefSkipSeconds();
        final int[] values = activity.getResources().getIntArray(R.array.seek_delta_values);
        final String[] choices = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            if (skipSecs == values[i]) {
                checked = i;
            }
            choices[i] = String.valueOf(values[i]) + " " + activity.getString(R.string.time_seconds);
        }

        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setTitle(direction.getTitleResourceID());
        builder.setSingleChoiceItems(choices, checked, null);
        builder.setNegativeButton(R.string.cancel_label, null);
        builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
            int choice = ((AlertDialog)dialog).getListView().getCheckedItemPosition();
            if (choice < 0 || choice >= values.length) {
                System.err.printf("Choice in showSkipPreference is out of bounds %d", choice);
            } else {
                direction.setPrefSkipSeconds(values[choice], activity);
            }
        });
        builder.create().show();
    }

    void setupGUI() {
        setContentView(getContentViewResourceId());
        sbPosition = findViewById(R.id.sbPosition);
        txtvPosition = findViewById(R.id.txtvPosition);

        SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
        showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
        Log.d("timeleft", showTimeLeft ? "true" : "false");
        txtvLength = findViewById(R.id.txtvLength);
        if (txtvLength != null) {
            txtvLength.setOnClickListener(v -> {
                showTimeLeft = !showTimeLeft;
                Playable media = controller.getMedia();
                if (media == null) {
                    return;
                }

                TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
                String length;
                if (showTimeLeft) {
                    int remainingTime = converter.convert(
                            media.getDuration() - media.getPosition());

                    length = "-" + Converter.getDurationStringLong(remainingTime);
                } else {
                    int duration = converter.convert(media.getDuration());
                    length = Converter.getDurationStringLong(duration);
                }
                txtvLength.setText(length);

                SharedPreferences.Editor editor = prefs.edit();
                editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft);
                editor.apply();
                Log.d("timeleft on click", showTimeLeft ? "true" : "false");
            });
        }

        butRev = findViewById(R.id.butRev);
        txtvRev = findViewById(R.id.txtvRev);
        if (txtvRev != null) {
            txtvRev.setText(String.valueOf(UserPreferences.getRewindSecs()));
        }
        butPlay = findViewById(R.id.butPlay);
        butFF = findViewById(R.id.butFF);
        txtvFF = findViewById(R.id.txtvFF);
        if (txtvFF != null) {
            txtvFF.setText(String.valueOf(UserPreferences.getFastForwardSecs()));
        }
        butSkip = findViewById(R.id.butSkip);

        // SEEKBAR SETUP

        sbPosition.setOnSeekBarChangeListener(this);

        // BUTTON SETUP

        if (butRev != null) {
            butRev.setOnClickListener(v -> onRewind());
            butRev.setOnLongClickListener(v -> {
                showSkipPreference(MediaplayerActivity.this, SkipDirection.SKIP_REWIND);
                return true;
            });
        }

        butPlay.setOnClickListener(v -> onPlayPause());

        if (butFF != null) {
            butFF.setOnClickListener(v -> onFastForward());
            butFF.setOnLongClickListener(v -> {
                showSkipPreference(MediaplayerActivity.this, SkipDirection.SKIP_FORWARD);
                return false;
            });
        }

        if (butSkip != null) {
            butSkip.setOnClickListener(v ->
                    IntentUtils.sendLocalBroadcast(MediaplayerActivity.this, PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
        }
    }

    void onRewind() {
        if (controller == null) {
            return;
        }
        int curr = controller.getPosition();
        controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000);
    }

    void onPlayPause() {
        if(controller == null) {
            return;
        }
        controller.init();
        controller.playPause();
    }

    void onFastForward() {
        if (controller == null) {
            return;
        }
        int curr = controller.getPosition();
        controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000);
    }

    protected abstract int getContentViewResourceId();

    private void handleError(int errorCode) {
        final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this);
        errorDialog.setTitle(R.string.error_label);
        errorDialog.setMessage(MediaPlayerError.getErrorString(this, errorCode));
        errorDialog.setNeutralButton("OK",
                (dialog, which) -> {
                    dialog.dismiss();
                    finish();
                }
        );
        errorDialog.create().show();
    }

    private float prog;

    @Override
    public void onProgressChanged (SeekBar seekBar,int progress, boolean fromUser) {
        if (controller == null || txtvLength == null) {
            return;
        }
        prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, txtvPosition);
        if (showTimeLeft && prog != 0) {
            int duration = controller.getDuration();
            TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
            int timeLeft = converter.convert(duration - (int) (prog * duration));
            String length = "-" + Converter.getDurationStringLong(timeLeft);
            txtvLength.setText(length);
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        if (controller != null) {
            controller.onSeekBarStartTrackingTouch(seekBar);
        }
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        if (controller != null) {
            controller.onSeekBarStopTrackingTouch(seekBar, prog);
        }
    }

    private void checkFavorite() {
        Playable playable = controller.getMedia();
        if (!(playable instanceof FeedMedia)) {
            return;
        }
        FeedItem feedItem = ((FeedMedia) playable).getItem();
        if (feedItem == null) {
            return;
        }
        if (disposable != null) {
            disposable.dispose();
        }
        disposable = Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId()))
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                item -> {
                    boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE);
                    if (isFavorite != isFav) {
                        isFavorite = isFav;
                        invalidateOptionsMenu();
                    }
                }, error -> Log.e(TAG, Log.getStackTraceString(error)));
    }

    void playExternalMedia(Intent intent, MediaType type) {
        if (intent == null || intent.getData() == null) {
            return;
        }
        if (Build.VERSION.SDK_INT >= 23
                && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show();
            } else {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                        REQUEST_CODE_STORAGE);
            }
            return;
        }

        Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath());
        ExternalMedia media = new ExternalMedia(intent.getData().getPath(), type);

        new PlaybackServiceStarter(this, media)
                .startWhenPrepared(true)
                .shouldStream(false)
                .prepareImmediately(true)
                .start();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == REQUEST_CODE_STORAGE) {
            if (grantResults.length <= 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show();
            }
        }
    }
}

I am not totally sure but I think it might be better to have all of this directly in controller.getPlaybackSpeed (as a fallback if it is not connected to the playback service yet).