Merge pull request #5162 from ByteHamster/remove-audio-only-code

VideoPlayer rework
This commit is contained in:
ByteHamster 2021-05-15 11:35:40 +02:00 committed by GitHub
commit af6af2fb78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 515 additions and 888 deletions

View File

@ -31,6 +31,6 @@ public class VideoplayerActivityTest {
@Test @Test
public void testStartActivity() throws Exception { public void testStartActivity() throws Exception {
activityTestRule.launchActivity(new Intent()); activityTestRule.launchActivity(new Intent());
onView(withId(R.id.videoframe)).check(matches(isDisplayed())); onView(withId(R.id.videoPlayerContainer)).check(matches(isDisplayed()));
} }
} }

View File

@ -1,658 +0,0 @@
package de.danoeh.antennapod.activity;
import android.annotation.TargetApi;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Bundle;
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 com.bumptech.glide.Glide;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.view.PlayButton;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.text.NumberFormat;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView;
import androidx.core.app.ActivityOptionsCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
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.Converter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
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.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
import de.danoeh.antennapod.dialog.ShareDialog;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
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";
PlaybackController controller;
private TextView txtvPosition;
private TextView txtvLength;
SeekBar sbPosition;
private ImageButton butRev;
private TextView txtvRev;
private PlayButton butPlay;
private ImageButton butFF;
private TextView txtvFF;
private ImageButton butSkip;
private CardView cardViewSeek;
private TextView txtvSeek;
private boolean showTimeLeft = false;
private boolean isFavorite = false;
private Disposable disposable;
private PlaybackController newPlaybackController() {
return new PlaybackController(this) {
@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
protected void updatePlayButtonShowsPlay(boolean showPlay) {
butPlay.setIsShowPlay(showPlay);
}
@Override
public void loadMediaInfo() {
MediaplayerActivity.this.loadMediaInfo();
}
@Override
public void onAwaitingVideoSurface() {
MediaplayerActivity.this.onAwaitingVideoSurface();
}
@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);
}
};
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
onPositionObserverUpdate();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onPlaybackServiceChanged(ServiceEvent event) {
if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) {
finish();
}
}
private void onPlaybackSpeedChange() {
updatePlaybackSpeedButtonText();
}
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 void onBufferStart() {
}
/**
* Should be used to hide the view that was showing the 'buffering'-message.
*/
protected 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();
EventBus.getDefault().register(this);
}
@Override
protected void onStop() {
Log.d(TAG, "onStop()");
if (controller != null) {
controller.release();
controller = null; // prevent leak
}
if (disposable != null) {
disposable.dispose();
}
EventBus.getDefault().unregister(this);
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);
requestCastButton(menu);
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 instanceof FeedMedia);
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed
boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null );
menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink);
boolean isItemAndHasLink = isFeedMedia &&
ShareUtils.hasLinkToShare(((FeedMedia) media).getItem());
boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownload_url() != null;
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);
}
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive());
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive());
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) {
ActivityOptionsCompat options = ActivityOptionsCompat
.makeSceneTransitionAnimation(MediaplayerActivity.this, cover, "coverTransition");
startActivity(intent, options.toBundle());
} else {
startActivity(intent);
}
finish();
return true;
} else {
if (media != null) {
final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem
switch (item.getItemId()) {
case R.id.add_to_favorites_item:
if (feedItem != null) {
DBWriter.addFavoriteItem(feedItem);
isFavorite = true;
invalidateOptionsMenu();
}
break;
case R.id.remove_from_favorites_item:
if (feedItem != null) {
DBWriter.removeFavoriteItem(feedItem);
isFavorite = false;
invalidateOptionsMenu();
}
break;
case R.id.disable_sleeptimer_item: // Fall-through
case R.id.set_sleeptimer_item:
new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog");
break;
case R.id.audio_controls:
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance();
dialog.show(getSupportFragmentManager(), "playback_controls");
break;
case R.id.open_feed_item:
if (feedItem != null) {
Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId());
startActivity(intent);
}
break;
case R.id.visit_website_item:
IntentUtils.openInBrowser(MediaplayerActivity.this, getWebsiteLinkWithFallback(media));
break;
case R.id.share_item:
if (feedItem != null) {
ShareDialog shareDialog = ShareDialog.newInstance(feedItem);
shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog");
}
break;
default:
return false;
}
return true;
} else {
return false;
}
}
}
private static String getWebsiteLinkWithFallback(Playable media) {
if (media == null) {
return null;
} else if (StringUtils.isNotBlank(media.getWebsiteLink())) {
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();
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.
*/
void loadMediaInfo() {
Log.d(TAG, "loadMediaInfo()");
if (controller == null || controller.getMedia() == null) {
return;
}
showTimeLeft = UserPreferences.shouldShowRemainingTime();
onPositionObserverUpdate();
checkFavorite();
updatePlaybackSpeedButton();
}
void updatePlaybackSpeedButton() {
// Only meaningful on AudioplayerActivity, where it is overridden.
}
void updatePlaybackSpeedButtonText() {
// Only meaningful on AudioplayerActivity, where it is overridden.
}
void setupGUI() {
setContentView(getContentViewResourceId());
sbPosition = findViewById(R.id.sbPosition);
txtvPosition = findViewById(R.id.txtvPosition);
cardViewSeek = findViewById(R.id.cardViewSeek);
txtvSeek = findViewById(R.id.txtvSeek);
showTimeLeft = UserPreferences.shouldShowRemainingTime();
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);
UserPreferences.setShowRemainTimeSetting(showTimeLeft);
Log.d("timeleft on click", showTimeLeft ? "true" : "false");
});
}
butRev = findViewById(R.id.butRev);
txtvRev = findViewById(R.id.txtvRev);
if (txtvRev != null) {
txtvRev.setText(NumberFormat.getInstance().format(UserPreferences.getRewindSecs()));
}
butPlay = findViewById(R.id.butPlay);
butPlay.setIsVideoScreen(true);
butFF = findViewById(R.id.butFF);
txtvFF = findViewById(R.id.txtvFF);
if (txtvFF != null) {
txtvFF.setText(NumberFormat.getInstance().format(UserPreferences.getFastForwardSecs()));
}
butSkip = findViewById(R.id.butSkip);
// SEEKBAR SETUP
sbPosition.setOnSeekBarChangeListener(this);
// BUTTON SETUP
if (butRev != null) {
butRev.setOnClickListener(v -> onRewind());
butRev.setOnLongClickListener(v -> {
SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this,
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev);
return true;
});
}
butPlay.setOnClickListener(v -> onPlayPause());
if (butFF != null) {
butFF.setOnClickListener(v -> onFastForward());
butFF.setOnLongClickListener(v -> {
SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this,
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF);
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;
}
if (fromUser) {
prog = progress / ((float) seekBar.getMax());
TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
int position = converter.convert((int) (prog * controller.getDuration()));
txtvSeek.setText(Converter.getDurationStringLong(position));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
cardViewSeek.setScaleX(.8f);
cardViewSeek.setScaleY(.8f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (controller != null) {
controller.seekTo((int) (prog * controller.getDuration()));
}
cardViewSeek.setScaleX(1f);
cardViewSeek.setScaleY(1f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start();
}
private void checkFavorite() {
FeedItem feedItem = getFeedItem(controller.getMedia());
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)));
}
@Nullable
private static FeedItem getFeedItem(@Nullable Playable playable) {
if (playable instanceof FeedMedia) {
return ((FeedMedia) playable).getItem();
} else {
return null;
}
}
}

View File

@ -1,53 +1,79 @@
package de.danoeh.antennapod.activity; package de.danoeh.antennapod.activity;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.view.Gravity; import android.os.Looper;
import android.view.KeyEvent;
import android.view.animation.AlphaAnimation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.widget.EditText;
import android.widget.ImageView;
import androidx.core.view.WindowCompat;
import androidx.appcompat.app.ActionBar;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar; import android.widget.SeekBar;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference; import androidx.appcompat.app.AlertDialog;
import java.util.concurrent.atomic.AtomicBoolean; import androidx.core.view.WindowCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.bumptech.glide.Glide;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
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.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.databinding.VideoplayerActivityBinding;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
import de.danoeh.antennapod.dialog.ShareDialog;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.view.AspectRatioVideoView; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/** /**
* Activity for playing video files. * Activity for playing video files.
*/ */
public class VideoplayerActivity extends MediaplayerActivity { public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.OnSeekBarChangeListener {
private static final String TAG = "VideoplayerActivity"; private static final String TAG = "VideoplayerActivity";
/** /**
@ -57,36 +83,39 @@ public class VideoplayerActivity extends MediaplayerActivity {
private boolean videoSurfaceCreated = false; private boolean videoSurfaceCreated = false;
private boolean destroyingDueToReload = false; private boolean destroyingDueToReload = false;
private long lastScreenTap = 0; private long lastScreenTap = 0;
private Handler videoControlsHider = new Handler(Looper.getMainLooper());
private VideoControlsHider videoControlsHider = new VideoControlsHider(this); private VideoplayerActivityBinding viewBinding;
private PlaybackController controller;
private final AtomicBoolean isSetup = new AtomicBoolean(false); private boolean showTimeLeft = false;
private boolean isFavorite = false;
private LinearLayout controls; private Disposable disposable;
private LinearLayout videoOverlay; private float prog;
private AspectRatioVideoView videoview;
private ProgressBar progressIndicator;
private FrameLayout videoframe;
private ImageView skipAnimationView;
@Override
protected void chooseTheme() {
setTheme(R.style.Theme_AntennaPod_VideoPlayer);
}
@SuppressLint("AppCompatMethod") @SuppressLint("AppCompatMethod")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); // has to be called before setting layout content // has to be called before setting layout content
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY);
setTheme(R.style.Theme_AntennaPod_VideoPlayer);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate()");
StorageUtils.checkStorageAvailability(this);
getWindow().setFormat(PixelFormat.TRANSPARENT);
viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this));
setContentView(viewBinding.getRoot());
setupView();
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
StorageUtils.checkStorageAvailability(this);
if (PlaybackService.isCasting()) { if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this); Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {
@ -99,11 +128,20 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override @Override
protected void onStop() { protected void onStop() {
if (controller != null) {
controller.release();
controller = null; // prevent leak
}
if (disposable != null) {
disposable.dispose();
}
EventBus.getDefault().unregister(this);
super.onStop(); super.onStop();
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
videoControlsHider.stop(); videoControlsHider.removeCallbacks(hideVideoControls);
} }
progressIndicator.setVisibility(View.GONE); // Controller released; we will not receive buffering updates // Controller released; we will not receive buffering updates
viewBinding.progressBar.setVisibility(View.GONE);
} }
@Override @Override
@ -114,6 +152,16 @@ public class VideoplayerActivity extends MediaplayerActivity {
} }
} }
@Override
protected void onStart() {
super.onStart();
controller = newPlaybackController();
controller.init();
loadMediaInfo();
onPositionObserverUpdate();
EventBus.getDefault().register(this);
}
@Override @Override
protected void onPause() { protected void onPause() {
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
@ -124,16 +172,103 @@ public class VideoplayerActivity extends MediaplayerActivity {
super.onPause(); super.onPause();
} }
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override @Override
protected void onDestroy() { public void onTrimMemory(int level) {
videoControlsHider.stop(); super.onTrimMemory(level);
videoControlsHider = null; Glide.get(this).trimMemory(level);
super.onDestroy();
} }
@Override @Override
public void onLowMemory() {
super.onLowMemory();
Glide.get(this).clearMemory();
}
private PlaybackController newPlaybackController() {
return new PlaybackController(this) {
@Override
public void onPositionObserverUpdate() {
VideoplayerActivity.this.onPositionObserverUpdate();
}
@Override
public void onBufferStart() {
viewBinding.progressBar.setVisibility(View.VISIBLE);
}
@Override
public void onBufferEnd() {
viewBinding.progressBar.setVisibility(View.INVISIBLE);
}
@Override
public void onBufferUpdate(float progress) {
viewBinding.sbPosition.setSecondaryProgress((int) (progress * viewBinding.sbPosition.getMax()));
}
@Override
public void handleError(int code) {
final AlertDialog.Builder errorDialog = new AlertDialog.Builder(VideoplayerActivity.this);
errorDialog.setTitle(R.string.error_label);
errorDialog.setMessage(MediaPlayerError.getErrorString(VideoplayerActivity.this, code));
errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish());
errorDialog.show();
}
@Override
public void onReloadNotification(int code) {
VideoplayerActivity.this.onReloadNotification(code);
}
@Override
public void onSleepTimerUpdate() {
supportInvalidateOptionsMenu();
}
@Override
protected void updatePlayButtonShowsPlay(boolean showPlay) {
viewBinding.playButton.setIsShowPlay(showPlay);
}
@Override
public void loadMediaInfo() {
VideoplayerActivity.this.loadMediaInfo();
}
@Override
public void onAwaitingVideoSurface() {
setupVideoAspectRatio();
if (videoSurfaceCreated && controller != null) {
Log.d(TAG, "Videosurface already created, setting videosurface now");
controller.setVideoSurface(viewBinding.videoView.getHolder());
}
}
@Override
public void onPlaybackEnd() {
finish();
}
@Override
protected void setScreenOn(boolean enable) {
if (enable) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
};
}
protected void loadMediaInfo() { protected void loadMediaInfo() {
super.loadMediaInfo(); Log.d(TAG, "loadMediaInfo()");
if (controller == null || controller.getMedia() == null) {
return;
}
showTimeLeft = UserPreferences.shouldShowRemainingTime();
onPositionObserverUpdate();
checkFavorite();
Playable media = controller.getMedia(); Playable media = controller.getMedia();
if (media != null) { if (media != null) {
getSupportActionBar().setSubtitle(media.getEpisodeTitle()); getSupportActionBar().setSubtitle(media.getEpisodeTitle());
@ -141,48 +276,79 @@ public class VideoplayerActivity extends MediaplayerActivity {
} }
} }
@Override protected void setupView() {
protected void setupGUI() { showTimeLeft = UserPreferences.shouldShowRemainingTime();
if (isSetup.getAndSet(true)) { Log.d("timeleft", showTimeLeft ? "true" : "false");
viewBinding.durationLabel.setOnClickListener(v -> {
showTimeLeft = !showTimeLeft;
Playable media = controller.getMedia();
if (media == null) {
return; return;
} }
super.setupGUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
controls = findViewById(R.id.controls); String length;
videoOverlay = findViewById(R.id.overlay); if (showTimeLeft) {
videoview = findViewById(R.id.videoview); int remainingTime = converter.convert(media.getDuration() - media.getPosition());
videoframe = findViewById(R.id.videoframe); length = "-" + Converter.getDurationStringLong(remainingTime);
progressIndicator = findViewById(R.id.progressIndicator); } else {
skipAnimationView = findViewById(R.id.skip_animation); int duration = converter.convert(media.getDuration());
videoview.getHolder().addCallback(surfaceHolderCallback); length = Converter.getDurationStringLong(duration);
videoframe.setOnTouchListener(onVideoviewTouched); }
videoOverlay.setOnTouchListener((view, motionEvent) -> true); // To suppress touches directly below the slider viewBinding.durationLabel.setText(length);
videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
videoOverlay.setFitsSystemWindows(true); UserPreferences.setShowRemainTimeSetting(showTimeLeft);
Log.d("timeleft on click", showTimeLeft ? "true" : "false");
});
viewBinding.sbPosition.setOnSeekBarChangeListener(this);
viewBinding.rewindButton.setOnClickListener(v -> onRewind());
viewBinding.rewindButton.setOnLongClickListener(v -> {
SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this,
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null);
return true;
});
viewBinding.playButton.setIsVideoScreen(true);
viewBinding.playButton.setOnClickListener(v -> onPlayPause());
viewBinding.fastForwardButton.setOnClickListener(v -> onFastForward());
viewBinding.fastForwardButton.setOnLongClickListener(v -> {
SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this,
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null);
return false;
});
// To suppress touches directly below the slider
viewBinding.bottomControlsContainer.setOnTouchListener((view, motionEvent) -> true);
viewBinding.bottomControlsContainer.setFitsSystemWindows(true);
viewBinding.videoView.getHolder().addCallback(surfaceHolderCallback);
viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
setupVideoControlsToggler(); setupVideoControlsToggler();
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN); WindowManager.LayoutParams.FLAG_FULLSCREEN);
videoframe.getViewTreeObserver().addOnGlobalLayoutListener(() -> viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched);
videoview.setAvailableSize(videoframe.getWidth(), videoframe.getHeight())); viewBinding.videoPlayerContainer.getViewTreeObserver().addOnGlobalLayoutListener(() ->
viewBinding.videoView.setAvailableSize(
viewBinding.videoPlayerContainer.getWidth(), viewBinding.videoPlayerContainer.getHeight()));
} }
@Override private final Runnable hideVideoControls = () -> {
protected void onAwaitingVideoSurface() { if (videoControlsShowing) {
setupVideoAspectRatio(); Log.d(TAG, "Hiding video controls");
if (videoSurfaceCreated && controller != null) { getSupportActionBar().hide();
Log.d(TAG, "Videosurface already created, setting videosurface now"); hideVideoControls(true);
controller.setVideoSurface(videoview.getHolder()); videoControlsShowing = false;
}
} }
};
private final View.OnTouchListener onVideoviewTouched = (v, event) -> { private final View.OnTouchListener onVideoviewTouched = (v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getAction() != MotionEvent.ACTION_DOWN) {
return false;
}
if (PictureInPictureUtil.isInPictureInPictureMode(this)) { if (PictureInPictureUtil.isInPictureInPictureMode(this)) {
return true; return true;
} }
videoControlsHider.stop(); videoControlsHider.removeCallbacks(hideVideoControls);
if (System.currentTimeMillis() - lastScreenTap < 300) { if (System.currentTimeMillis() - lastScreenTap < 300) {
if (event.getX() > v.getMeasuredWidth() / 2.0f) { if (event.getX() > v.getMeasuredWidth() / 2.0f) {
@ -207,9 +373,6 @@ public class VideoplayerActivity extends MediaplayerActivity {
lastScreenTap = System.currentTimeMillis(); lastScreenTap = System.currentTimeMillis();
return true; return true;
} else {
return false;
}
}; };
private void showSkipAnimation(boolean isForward) { private void showSkipAnimation(boolean isForward) {
@ -220,18 +383,18 @@ public class VideoplayerActivity extends MediaplayerActivity {
skipAnimation.setFillAfter(false); skipAnimation.setFillAfter(false);
skipAnimation.setDuration(800); skipAnimation.setDuration(800);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) skipAnimationView.getLayoutParams(); FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewBinding.skipAnimationImage.getLayoutParams();
if (isForward) { if (isForward) {
skipAnimationView.setImageResource(R.drawable.ic_fast_forward_video_white); viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white);
params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
} else { } else {
skipAnimationView.setImageResource(R.drawable.ic_fast_rewind_video_white); viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white);
params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
} }
skipAnimationView.setVisibility(View.VISIBLE); viewBinding.skipAnimationImage.setVisibility(View.VISIBLE);
skipAnimationView.setLayoutParams(params); viewBinding.skipAnimationImage.setLayoutParams(params);
skipAnimationView.startAnimation(skipAnimation); viewBinding.skipAnimationImage.startAnimation(skipAnimation);
skipAnimation.setAnimationListener(new Animation.AnimationListener() { skipAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override @Override
public void onAnimationStart(Animation animation) { public void onAnimationStart(Animation animation) {
@ -239,7 +402,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override @Override
public void onAnimationEnd(Animation animation) { public void onAnimationEnd(Animation animation) {
skipAnimationView.setVisibility(View.GONE); viewBinding.skipAnimationImage.setVisibility(View.GONE);
} }
@Override @Override
@ -249,10 +412,9 @@ public class VideoplayerActivity extends MediaplayerActivity {
}); });
} }
@SuppressLint("NewApi")
private void setupVideoControlsToggler() { private void setupVideoControlsToggler() {
videoControlsHider.stop(); videoControlsHider.removeCallbacks(hideVideoControls);
videoControlsHider.start(); videoControlsHider.postDelayed(hideVideoControls, 2500);
} }
private void setupVideoAspectRatio() { private void setupVideoAspectRatio() {
@ -260,7 +422,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
Pair<Integer, Integer> videoSize = controller.getVideoSize(); Pair<Integer, Integer> videoSize = controller.getVideoSize();
if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second);
videoview.setVideoSize(videoSize.first, videoSize.second); viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second);
} else { } else {
Log.e(TAG, "Could not determine video size"); Log.e(TAG, "Could not determine video size");
} }
@ -270,7 +432,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
private void toggleVideoControlsVisibility() { private void toggleVideoControlsVisibility() {
if (videoControlsShowing) { if (videoControlsShowing) {
getSupportActionBar().hide(); getSupportActionBar().hide();
hideVideoControls(); hideVideoControls(true);
} else { } else {
getSupportActionBar().show(); getSupportActionBar().show();
showVideoControls(); showVideoControls();
@ -278,29 +440,35 @@ public class VideoplayerActivity extends MediaplayerActivity {
videoControlsShowing = !videoControlsShowing; videoControlsShowing = !videoControlsShowing;
} }
@Override void onRewind() {
protected void onRewind() { if (controller == null) {
super.onRewind(); return;
}
int curr = controller.getPosition();
controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000);
setupVideoControlsToggler(); setupVideoControlsToggler();
} }
@Override void onPlayPause() {
protected void onPlayPause() { if (controller == null) {
super.onPlayPause(); return;
}
controller.playPause();
setupVideoControlsToggler(); setupVideoControlsToggler();
} }
@Override void onFastForward() {
protected void onFastForward() { if (controller == null) {
super.onFastForward(); return;
}
int curr = controller.getPosition();
controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000);
setupVideoControlsToggler(); setupVideoControlsToggler();
} }
private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
@Override @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
int height) {
holder.setFixedSize(width, height); holder.setFixedSize(width, height);
} }
@ -326,8 +494,6 @@ public class VideoplayerActivity extends MediaplayerActivity {
} }
}; };
@Override
protected void onReloadNotification(int notificationCode) { protected void onReloadNotification(int notificationCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && PictureInPictureUtil.isInPictureInPictureMode(this)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO
@ -344,80 +510,83 @@ public class VideoplayerActivity extends MediaplayerActivity {
} }
} }
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
super.onStartTrackingTouch(seekBar);
videoControlsHider.stop();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
setupVideoControlsToggler();
}
@Override
protected void onBufferStart() {
progressIndicator.setVisibility(View.VISIBLE);
}
@Override
protected void onBufferEnd() {
progressIndicator.setVisibility(View.INVISIBLE);
}
@SuppressLint("NewApi")
private void showVideoControls() { private void showVideoControls() {
videoOverlay.setVisibility(View.VISIBLE); viewBinding.bottomControlsContainer.setVisibility(View.VISIBLE);
controls.setVisibility(View.VISIBLE); viewBinding.controlsContainer.setVisibility(View.VISIBLE);
final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in); final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in);
if (animation != null) { if (animation != null) {
videoOverlay.startAnimation(animation); viewBinding.bottomControlsContainer.startAnimation(animation);
controls.startAnimation(animation); viewBinding.controlsContainer.startAnimation(animation);
} }
videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
} }
@SuppressLint("NewApi")
private void hideVideoControls(boolean showAnimation) { private void hideVideoControls(boolean showAnimation) {
if (showAnimation) { if (showAnimation) {
final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out); final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
if (animation != null) { if (animation != null) {
videoOverlay.startAnimation(animation); viewBinding.bottomControlsContainer.startAnimation(animation);
controls.startAnimation(animation); viewBinding.controlsContainer.startAnimation(animation);
} }
} }
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
videoOverlay.setFitsSystemWindows(true); viewBinding.bottomControlsContainer.setFitsSystemWindows(true);
videoOverlay.setVisibility(View.GONE); viewBinding.bottomControlsContainer.setVisibility(View.GONE);
controls.setVisibility(View.GONE); viewBinding.controlsContainer.setVisibility(View.GONE);
} }
private void hideVideoControls() { @Subscribe(threadMode = ThreadMode.MAIN)
hideVideoControls(true); public void onEventMainThread(PlaybackPositionEvent event) {
onPositionObserverUpdate();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onPlaybackServiceChanged(ServiceEvent event) {
if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) {
finish();
}
} }
@Override @Override
protected int getContentViewResourceId() { public boolean onCreateOptionsMenu(Menu menu) {
return R.layout.videoplayer_activity; super.onCreateOptionsMenu(menu);
} requestCastButton(menu);
MenuInflater inflater = getMenuInflater();
@Override inflater.inflate(R.menu.mediaplayer, menu);
protected void setScreenOn(boolean enable) { return true;
super.setScreenOn(enable);
if (enable) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
} }
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
if (controller == null) {
return false;
}
Playable media = controller.getMedia();
boolean isFeedMedia = (media instanceof FeedMedia);
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed
boolean hasWebsiteLink = getWebsiteLinkWithFallback(media) != null;
menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink);
boolean isItemAndHasLink = isFeedMedia && ShareUtils.hasLinkToShare(((FeedMedia) media).getItem());
boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownload_url() != null;
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);
}
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive());
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive());
if (PictureInPictureUtil.supportsPictureInPicture(this)) { if (PictureInPictureUtil.supportsPictureInPicture(this)) {
menu.findItem(R.id.player_go_to_picture_in_picture).setVisible(true); menu.findItem(R.id.player_go_to_picture_in_picture).setVisible(true);
} }
@ -431,7 +600,161 @@ public class VideoplayerActivity extends MediaplayerActivity {
compatEnterPictureInPicture(); compatEnterPictureInPicture();
return true; return true;
} }
return super.onOptionsItemSelected(item); if (item.getItemId() == android.R.id.home) {
Intent intent = new Intent(VideoplayerActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
return true;
}
if (controller == null) {
return false;
}
Playable media = controller.getMedia();
if (media == null) {
return false;
}
final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem
if (item.getItemId() == R.id.add_to_favorites_item && feedItem != null) {
DBWriter.addFavoriteItem(feedItem);
isFavorite = true;
invalidateOptionsMenu();
} else if (item.getItemId() == R.id.remove_from_favorites_item && feedItem != null) {
DBWriter.removeFavoriteItem(feedItem);
isFavorite = false;
invalidateOptionsMenu();
} else if (item.getItemId() == R.id.disable_sleeptimer_item
|| item.getItemId() == R.id.set_sleeptimer_item) {
new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog");
} else if (item.getItemId() == R.id.audio_controls) {
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance();
dialog.show(getSupportFragmentManager(), "playback_controls");
} else if (item.getItemId() == R.id.open_feed_item && feedItem != null) {
Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId());
startActivity(intent);
} else if (item.getItemId() == R.id.visit_website_item) {
IntentUtils.openInBrowser(VideoplayerActivity.this, getWebsiteLinkWithFallback(media));
} else if (item.getItemId() == R.id.share_item && feedItem != null) {
ShareDialog shareDialog = ShareDialog.newInstance(feedItem);
shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog");
} else {
return false;
}
return true;
}
private static String getWebsiteLinkWithFallback(Playable media) {
if (media == null) {
return null;
} else if (StringUtils.isNotBlank(media.getWebsiteLink())) {
return media.getWebsiteLink();
} else if (media instanceof FeedMedia) {
return FeedItemUtil.getLinkWithFallback(((FeedMedia) media).getItem());
}
return null;
}
void onPositionObserverUpdate() {
if (controller == 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;
}
viewBinding.positionLabel.setText(Converter.getDurationStringLong(currentPosition));
if (showTimeLeft) {
viewBinding.durationLabel.setText("-" + Converter.getDurationStringLong(remainingTime));
} else {
viewBinding.durationLabel.setText(Converter.getDurationStringLong(duration));
}
updateProgressbarPosition(currentPosition, duration);
}
private void updateProgressbarPosition(int position, int duration) {
Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")");
float progress = ((float) position) / duration;
viewBinding.sbPosition.setProgress((int) (progress * viewBinding.sbPosition.getMax()));
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (controller == null) {
return;
}
if (fromUser) {
prog = progress / ((float) seekBar.getMax());
TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
int position = converter.convert((int) (prog * controller.getDuration()));
viewBinding.seekPositionLabel.setText(Converter.getDurationStringLong(position));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
viewBinding.seekCardView.setScaleX(.8f);
viewBinding.seekCardView.setScaleY(.8f);
viewBinding.seekCardView.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start();
videoControlsHider.removeCallbacks(hideVideoControls);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (controller != null) {
controller.seekTo((int) (prog * controller.getDuration()));
}
viewBinding.seekCardView.setScaleX(1f);
viewBinding.seekCardView.setScaleY(1f);
viewBinding.seekCardView.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start();
setupVideoControlsToggler();
}
private void checkFavorite() {
FeedItem feedItem = getFeedItem(controller.getMedia());
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)));
}
@Nullable
private static FeedItem getFeedItem(@Nullable Playable playable) {
if (playable instanceof FeedMedia) {
return ((FeedMedia) playable).getItem();
} else {
return null;
}
} }
private void compatEnterPictureInPicture() { private void compatEnterPictureInPicture() {
@ -442,43 +765,6 @@ public class VideoplayerActivity extends MediaplayerActivity {
} }
} }
private static class VideoControlsHider extends Handler {
private static final int DELAY = 2500;
private WeakReference<VideoplayerActivity> activity;
VideoControlsHider(VideoplayerActivity activity) {
this.activity = new WeakReference<>(activity);
}
private final Runnable hideVideoControls = () -> {
VideoplayerActivity vpa = activity != null ? activity.get() : null;
if (vpa == null) {
return;
}
if (vpa.videoControlsShowing) {
Log.d(TAG, "Hiding video controls");
ActionBar actionBar = vpa.getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
vpa.hideVideoControls();
vpa.videoControlsShowing = false;
}
};
public void start() {
this.postDelayed(hideVideoControls, DELAY);
}
void stop() {
this.removeCallbacks(hideVideoControls);
}
}
//Hardware keyboard support //Hardware keyboard support
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {

View File

@ -6,16 +6,16 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black" android:background="@color/black"
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/videoframe"> android:id="@+id/videoPlayerContainer">
<de.danoeh.antennapod.view.AspectRatioVideoView <de.danoeh.antennapod.view.AspectRatioVideoView
android:id="@+id/videoview" android:id="@+id/videoView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center" />
<ProgressBar <ProgressBar
android:id="@+id/progressIndicator" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -23,7 +23,7 @@
android:visibility="invisible" /> android:visibility="invisible" />
<LinearLayout <LinearLayout
android:id="@+id/controls" android:id="@+id/controlsContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -33,7 +33,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<ImageButton <ImageButton
android:id="@+id/butRev" android:id="@+id/rewindButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@ -42,7 +42,7 @@
app:srcCompat="@drawable/ic_fast_rewind_video_white" /> app:srcCompat="@drawable/ic_fast_rewind_video_white" />
<de.danoeh.antennapod.view.PlayButton <de.danoeh.antennapod.view.PlayButton
android:id="@+id/butPlay" android:id="@+id/playButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@ -51,7 +51,7 @@
app:srcCompat="@drawable/ic_pause_video_white" /> app:srcCompat="@drawable/ic_pause_video_white" />
<ImageButton <ImageButton
android:id="@+id/butFF" android:id="@+id/fastForwardButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@ -62,7 +62,7 @@
</LinearLayout> </LinearLayout>
<ImageView <ImageView
android:id="@+id/skip_animation" android:id="@+id/skipAnimationImage"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
@ -70,14 +70,14 @@
android:layout_gravity="center"/> android:layout_gravity="center"/>
<LinearLayout <LinearLayout
android:id="@+id/overlay" android:id="@+id/bottomControlsContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|center" android:layout_gravity="bottom|center"
android:orientation="vertical"> android:orientation="vertical">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/cardViewSeek" android:id="@+id/seekCardView"
android:alpha="0" android:alpha="0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -89,7 +89,7 @@
tools:alpha="1"> tools:alpha="1">
<TextView <TextView
android:id="@+id/txtvSeek" android:id="@+id/seekPositionLabel"
android:gravity="center" android:gravity="center"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -104,7 +104,6 @@
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<RelativeLayout <RelativeLayout
android:id="@+id/timecontrol"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"
android:background="#80000000" android:background="#80000000"
@ -112,7 +111,7 @@
android:paddingTop="8dp"> android:paddingTop="8dp">
<TextView <TextView
android:id="@+id/txtvPosition" android:id="@+id/positionLabel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
@ -129,7 +128,7 @@
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/txtvLength" android:id="@+id/durationLabel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
@ -149,10 +148,10 @@
android:id="@+id/sbPosition" android:id="@+id/sbPosition"
android:layout_width="0px" android:layout_width="0px"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/txtvLength" android:layout_toLeftOf="@+id/durationLabel"
android:layout_toStartOf="@+id/txtvLength" android:layout_toStartOf="@+id/durationLabel"
android:layout_toRightOf="@+id/txtvPosition" android:layout_toRightOf="@+id/positionLabel"
android:layout_toEndOf="@+id/txtvPosition" android:layout_toEndOf="@+id/positionLabel"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:max="500" /> android:max="500" />