diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 225f07dfd..beb6ad705 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -32,7 +32,9 @@ import java.util.concurrent.TimeoutException; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.allOf; @@ -193,4 +195,8 @@ public class EspressoTestUtils { } androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } + + public static Matcher actionBarOverflow() { + return allOf(isDisplayed(), withContentDescription("More options")); + } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java index 37d76bb6d..ec0d1fa64 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java @@ -1,6 +1,7 @@ package de.test.antennapod.ui; import android.content.Intent; +import android.view.View; import androidx.test.espresso.Espresso; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.runner.AndroidJUnit4; @@ -8,6 +9,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.fragment.QueueFragment; import de.test.antennapod.EspressoTestUtils; +import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -18,6 +20,7 @@ import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static de.test.antennapod.NthMatcher.first; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.endsWith; @@ -48,14 +51,14 @@ public class QueueFragmentTest { @Test public void testSortEmptyQueue() { - Espresso.openContextualActionModeOverflowMenu(); + onView(first(EspressoTestUtils.actionBarOverflow())).perform(click()); onView(withText(R.string.sort)).perform(click()); onView(withText(R.string.random)).perform(click()); } @Test public void testKeepEmptyQueueSorted() { - Espresso.openContextualActionModeOverflowMenu(); + onView(first(EspressoTestUtils.actionBarOverflow())).perform(click()); onView(withText(R.string.sort)).perform(click()); onView(withText(R.string.keep_sorted)).perform(click()); } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java index 194d51a3c..5565bf474 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java @@ -7,12 +7,14 @@ import android.preference.PreferenceManager; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.IgnoreOnCi; @@ -34,6 +36,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static de.test.antennapod.EspressoTestUtils.waitForView; +import static de.test.antennapod.NthMatcher.first; +import static org.hamcrest.Matchers.allOf; /** * User interface tests for changing the playback speed. @@ -43,10 +47,10 @@ import static de.test.antennapod.EspressoTestUtils.waitForView; public class SpeedChangeTest { @Rule - public ActivityTestRule activityRule - = new ActivityTestRule<>(AudioplayerActivity.class, false, false); + public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class, false, false); private UITestUtils uiTestUtils; private String[] availableSpeeds; + private PlaybackController controller; @Before public void setUp() throws Exception { @@ -70,7 +74,10 @@ public class SpeedChangeTest { UserPreferences.setPlaybackSpeedArray(availableSpeeds); EspressoTestUtils.tryKillPlaybackService(); - activityRule.launchActivity(new Intent()); + activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); + controller = new PlaybackController(activityRule.getActivity(), true); + controller.init(); + controller.getMedia(); // To load media } @After @@ -86,21 +93,21 @@ public class SpeedChangeTest { @Test public void testChangeSpeedPlaying() { onView(isRoot()).perform(waitForView(withId(R.id.butPlay), 1000)); - onView(withId(R.id.butPlay)).perform(click()); + controller.playPause(); Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> activityRule.getActivity().getPlaybackController().getStatus() == PlayerStatus.PLAYING); + -> controller.getStatus() == PlayerStatus.PLAYING); clickThroughSpeeds(); } @Test public void testChangeSpeedPaused() { onView(isRoot()).perform(waitForView(withId(R.id.butPlay), 1000)); - onView(withId(R.id.butPlay)).perform(click()); + controller.playPause(); Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> activityRule.getActivity().getPlaybackController().getStatus() == PlayerStatus.PLAYING); - onView(withId(R.id.butPlay)).perform(click()); + -> controller.getStatus() == PlayerStatus.PLAYING); + controller.playPause(); Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> activityRule.getActivity().getPlaybackController().getStatus() == PlayerStatus.PAUSED); + -> controller.getStatus() == PlayerStatus.PAUSED); clickThroughSpeeds(); } diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java index aff1d6ea4..98d506f65 100644 --- a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java +++ b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java @@ -2,6 +2,8 @@ package de.danoeh.antennapod.activity; import androidx.appcompat.app.AppCompatActivity; +import android.view.Menu; + /** * Activity that allows for showing the MediaRouter button whenever there's a cast device in the * network. @@ -9,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity; public abstract class CastEnabledActivity extends AppCompatActivity { public static final String TAG = "CastEnabledActivity"; - public final void requestCastButton(int showAsAction) { + public final void requestCastButton(Menu menu) { // no-op } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 62335b5e6..3f0df81f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,31 +73,6 @@ android:label="@string/app_name"> - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java new file mode 100644 index 000000000..1667006a5 --- /dev/null +++ b/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java @@ -0,0 +1,56 @@ +package com.google.android.material.bottomsheet; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import androidx.core.view.ViewCompat; +import androidx.viewpager.widget.ViewPager; + +import java.lang.ref.WeakReference; + +/** + * Override {@link #findScrollingChild(View)} to support {@link ViewPager}'s nested scrolling. + * In order to override package level method and field. + * This class put in the same package path where {@link BottomSheetBehavior} located. + * Source: https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3 + */ +public class ViewPagerBottomSheetBehavior extends BottomSheetBehavior { + + public ViewPagerBottomSheetBehavior() { + super(); + } + + public ViewPagerBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + View findScrollingChild(View view) { + if (ViewCompat.isNestedScrollingEnabled(view)) { + return view; + } + + if (view instanceof ViewPager) { + ViewPager viewPager = (ViewPager) view; + View currentViewPagerChild = viewPager.getChildAt(viewPager.getCurrentItem()); + if (currentViewPagerChild != null) { + return findScrollingChild(currentViewPagerChild); + } + } else if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View scrollingChild = findScrollingChild(group.getChildAt(i)); + if (scrollingChild != null) { + return scrollingChild; + } + } + } + return null; + } + + public void updateScrollingChild() { + final View scrollingChild = findScrollingChild(viewRef.get()); + nestedScrollingChildRef = new WeakReference<>(scrollingChild); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java deleted file mode 100644 index ae8634516..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ /dev/null @@ -1,136 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; - -import java.text.DecimalFormat; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Activity for playing audio files. - */ -public class AudioplayerActivity extends MediaplayerInfoActivity { - private static final String TAG = "AudioPlayerActivity"; - private static final float EPSILON = 0.001f; - - private final AtomicBoolean isSetup = new AtomicBoolean(false); - - @Override - protected void onResume() { - super.onResume(); - if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { - playExternalMedia(getIntent(), MediaType.AUDIO); - } else if (PlaybackService.isCasting()) { - Intent intent = PlaybackService.getPlayerActivityIntent(this); - if (intent.getComponent() != null - && !intent.getComponent().getClassName().equals(AudioplayerActivity.class.getName())) { - saveCurrentFragment(); - finish(); - startActivity(intent); - } - } - } - - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_CAST) { - Log.d(TAG, "ReloadNotification received, switching to Castplayer now"); - saveCurrentFragment(); - finish(); - startActivity(new Intent(this, CastplayerActivity.class)); - - } else { - super.onReloadNotification(notificationCode); - } - } - - @Override - protected void updatePlaybackSpeedButton() { - if (butPlaybackSpeed == null) { - return; - } - if (controller == null) { - butPlaybackSpeed.setVisibility(View.GONE); - txtvPlaybackSpeed.setVisibility(View.GONE); - return; - } - updatePlaybackSpeedButtonText(); - butPlaybackSpeed.setAlpha(controller.canSetPlaybackSpeed() ? 1.0f : 0.5f); - butPlaybackSpeed.setVisibility(View.VISIBLE); - txtvPlaybackSpeed.setVisibility(View.VISIBLE); - } - - @Override - protected void updatePlaybackSpeedButtonText() { - if (butPlaybackSpeed == null) { - return; - } - if (controller == null) { - butPlaybackSpeed.setVisibility(View.GONE); - txtvPlaybackSpeed.setVisibility(View.GONE); - return; - } - float speed = 1.0f; - if (controller.canSetPlaybackSpeed()) { - speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(controller.getMedia()); - } - String speedStr = new DecimalFormat("0.00").format(speed); - txtvPlaybackSpeed.setText(speedStr); - butPlaybackSpeed.setSpeed(speed); - } - - @Override - protected void setupGUI() { - if (isSetup.getAndSet(true)) { - return; - } - super.setupGUI(); - if (butPlaybackSpeed != null) { - butPlaybackSpeed.setOnClickListener(v -> { - if (controller == null) { - return; - } - if (controller.canSetPlaybackSpeed()) { - float[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); - float currentSpeed = controller.getCurrentPlaybackSpeedMultiplier(); - - int newSpeedIndex = 0; - while (newSpeedIndex < availableSpeeds.length - && availableSpeeds[newSpeedIndex] < currentSpeed + EPSILON) { - newSpeedIndex++; - } - - float newSpeed; - if (availableSpeeds.length == 0) { - newSpeed = 1.0f; - } else if (newSpeedIndex == availableSpeeds.length) { - newSpeed = availableSpeeds[0]; - } else { - newSpeed = availableSpeeds[newSpeedIndex]; - } - - PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(newSpeed); - UserPreferences.setPlaybackSpeed(newSpeed); - controller.setPlaybackSpeed(newSpeed); - onPositionObserverUpdate(); - } else { - VariableSpeedDialog.showGetPluginDialog(this); - } - }); - butPlaybackSpeed.setOnLongClickListener(v -> { - VariableSpeedDialog.showDialog(this); - return true; - }); - butPlaybackSpeed.setVisibility(View.VISIBLE); - txtvPlaybackSpeed.setVisibility(View.VISIBLE); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java deleted file mode 100644 index bbab235c8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java +++ /dev/null @@ -1,80 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.View; - -import java.util.concurrent.atomic.AtomicBoolean; - -import de.danoeh.antennapod.core.service.playback.PlaybackService; - -/** - * Activity for controlling the remote playback on a Cast device. - */ -public class CastplayerActivity extends MediaplayerInfoActivity { - private static final String TAG = "CastPlayerActivity"; - - private final AtomicBoolean isSetup = new AtomicBoolean(false); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (!PlaybackService.isCasting()) { - Intent intent = PlaybackService.getPlayerActivityIntent(this); - if (!intent.getComponent().getClassName().equals(CastplayerActivity.class.getName())) { - finish(); - startActivity(intent); - } - } - } - - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { - Log.d(TAG, "ReloadNotification received, switching to Audioplayer now"); - saveCurrentFragment(); - finish(); - startActivity(new Intent(this, AudioplayerActivity.class)); - } else { - super.onReloadNotification(notificationCode); - } - } - - @Override - protected void setupGUI() { - if(isSetup.getAndSet(true)) { - return; - } - super.setupGUI(); - if (butPlaybackSpeed != null) { - butPlaybackSpeed.setVisibility(View.GONE); - txtvPlaybackSpeed.setVisibility(View.GONE); - } - } - - @Override - protected void onResume() { - if (!PlaybackService.isCasting()) { - Intent intent = PlaybackService.getPlayerActivityIntent(this); - if (!intent.getComponent().getClassName().equals(CastplayerActivity.class.getName())) { - saveCurrentFragment(); - finish(); - startActivity(intent); - } - } - super.onResume(); - } - - @Override - protected void onBufferStart() { - //sbPosition.setIndeterminate(true); - sbPosition.setEnabled(false); - } - - @Override - protected void onBufferEnd() { - //sbPosition.setIndeterminate(false); - sbPosition.setEnabled(true); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index 872174403..403198bea 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -23,6 +23,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import com.bumptech.glide.Glide; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.MessageEvent; @@ -31,9 +32,9 @@ import de.danoeh.antennapod.core.util.Flavors; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; +import de.danoeh.antennapod.fragment.AudioPlayerFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.EpisodesFragment; -import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; @@ -41,6 +42,7 @@ import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.preferences.PreferenceUpgrader; +import de.danoeh.antennapod.view.LockableBottomSheetBehavior; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; @@ -61,12 +63,14 @@ public class MainActivity extends CastEnabledActivity { public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; public static final String EXTRA_FEED_ID = "fragment_feed_id"; + public static final String EXTRA_OPEN_PLAYER = "open_player"; private static final String SAVE_BACKSTACK_COUNT = "backstackCount"; private DrawerLayout drawerLayout; private View navDrawer; private ActionBarDrawerToggle drawerToggle; + private LockableBottomSheetBehavior sheetBehavior; private long lastBackButtonPressTime = 0; @NonNull @@ -111,15 +115,34 @@ public class MainActivity extends CastEnabledActivity { } } } - ExternalPlayerFragment externalPlayerFragment = new ExternalPlayerFragment(); - transaction.replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG); NavDrawerFragment navDrawerFragment = new NavDrawerFragment(); transaction.replace(R.id.navDrawerFragment, navDrawerFragment, NavDrawerFragment.TAG); - + AudioPlayerFragment audioPlayerFragment = new AudioPlayerFragment(); + transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG); transaction.commit(); checkFirstLaunch(); PreferenceUpgrader.checkUpgrades(this); + View bottomSheet = findViewById(R.id.audioplayerFragment); + sheetBehavior = (LockableBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet); + sheetBehavior.setPeekHeight((int) getResources().getDimension(R.dimen.external_player_height)); + sheetBehavior.setHideable(false); + sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View view, int state) { + + } + + @Override + public void onSlide(@NonNull View view, float slideOffset) { + AudioPlayerFragment audioPlayer = + (AudioPlayerFragment) getSupportFragmentManager().findFragmentByTag(AudioPlayerFragment.TAG); + float condensedSlideOffset = Math.max(0.0f, Math.min(0.1f, slideOffset - 0.5f)) / 0.1f; + audioPlayer.getExternalPlayerHolder().setAlpha(1 - condensedSlideOffset); + audioPlayer.getExternalPlayerHolder().setVisibility( + condensedSlideOffset > 0.99f ? View.GONE : View.VISIBLE); + } + }); } @Override @@ -151,6 +174,10 @@ public class MainActivity extends CastEnabledActivity { return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer); } + public LockableBottomSheetBehavior getBottomSheet() { + return sheetBehavior; + } + public void loadFragment(String tag, Bundle args) { Log.d(TAG, "loadFragment(tag: " + tag + ", args: " + args + ")"); Fragment fragment; @@ -301,29 +328,6 @@ public class MainActivity extends CastEnabledActivity { Glide.get(this).clearMemory(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - boolean retVal = super.onCreateOptionsMenu(menu); - if (Flavors.FLAVOR == Flavors.PLAY) { - switch (NavDrawerFragment.getLastNavFragment(this)) { - case QueueFragment.TAG: - case EpisodesFragment.TAG: - requestCastButton(MenuItem.SHOW_AS_ACTION_IF_ROOM); - return retVal; - case DownloadsFragment.TAG: - case PlaybackHistoryFragment.TAG: - case AddFeedFragment.TAG: - case SubscriptionFragment.TAG: - return retVal; - default: - requestCastButton(MenuItem.SHOW_AS_ACTION_NEVER); - return retVal; - } - } else { - return retVal; - } - } - @Override public boolean onOptionsItemSelected(MenuItem item) { if (drawerToggle.onOptionsItemSelected(item)) { @@ -342,6 +346,8 @@ public class MainActivity extends CastEnabledActivity { public void onBackPressed() { if (isDrawerOpen()) { drawerLayout.closeDrawer(navDrawer); + } else if (sheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } else if (getSupportFragmentManager().getBackStackEntryCount() != 0) { super.onBackPressed(); } else { @@ -400,9 +406,11 @@ public class MainActivity extends CastEnabledActivity { } else if (feedId > 0) { loadFeedFragmentById(feedId, args); } - // to avoid handling the intent twice when the configuration changes - setIntent(new Intent(MainActivity.this, MainActivity.class)); + } else if (intent.hasExtra(EXTRA_OPEN_PLAYER)) { + sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); } + // to avoid handling the intent twice when the configuration changes + setIntent(new Intent(MainActivity.this, MainActivity.class)); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index 698d7e295..55aadc60b 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -2,7 +2,6 @@ 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; @@ -52,6 +51,7 @@ 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.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SleepTimerDialog; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -140,16 +140,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements 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(); @@ -198,13 +188,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements onPositionObserverUpdate(); } - 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(); @@ -258,12 +241,16 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements * Should be used to inform the user that the PlaybackService is currently * buffering. */ - protected abstract void onBufferStart(); + protected void onBufferStart() { + + } /** * Should be used to hide the view that was showing the 'buffering'-message. */ - protected abstract void onBufferEnd(); + protected void onBufferEnd() { + + } private void onBufferUpdate(float progress) { if (sbPosition != null) { @@ -311,9 +298,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - if (Flavors.FLAVOR == Flavors.PLAY) { - requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS); - } + requestCastButton(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.mediaplayer, menu); return true; @@ -478,10 +463,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements */ 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; @@ -543,92 +524,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements // 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 getPrefSecsFn; - private final Function getTextViewFn; - private final Consumer 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 getPrefSecsFn, Function getTextViewFn, Consumer 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; - } - } - - public static 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); @@ -688,7 +583,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (butRev != null) { butRev.setOnClickListener(v -> onRewind()); butRev.setOnLongClickListener(v -> { - showSkipPreference(MediaplayerActivity.this, SkipDirection.SKIP_REWIND); + SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev); return true; }); } @@ -698,7 +594,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (butFF != null) { butFF.setOnClickListener(v -> onFastForward()); butFF.setOnLongClickListener(v -> { - showSkipPreference(MediaplayerActivity.this, SkipDirection.SKIP_FORWARD); + SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF); return false; }); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java deleted file mode 100644 index 1a7631813..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java +++ /dev/null @@ -1,282 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.os.Bundle; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.widget.Toolbar; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; -import androidx.viewpager.widget.ViewPager; -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.fragment.ChaptersFragment; -import de.danoeh.antennapod.fragment.CoverFragment; -import de.danoeh.antennapod.fragment.ItemDescriptionFragment; -import de.danoeh.antennapod.fragment.NavDrawerFragment; -import de.danoeh.antennapod.view.PagerIndicatorView; -import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -/** - * Activity for playing files that do not require a video surface. - */ -public abstract class MediaplayerInfoActivity extends MediaplayerActivity { - - private static final String TAG = "MediaplayerInfoActivity"; - - private static final int POS_COVER = 0; - private static final int POS_DESCR = 1; - private static final int POS_CHAPTERS = 2; - private static final int NUM_CONTENT_FRAGMENTS = 3; - - private static final String PREFS = "AudioPlayerActivityPreferences"; - private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition"; - - PlaybackSpeedIndicatorView butPlaybackSpeed; - TextView txtvPlaybackSpeed; - private DrawerLayout drawerLayout; - private View navDrawer; - private ActionBarDrawerToggle drawerToggle; - private ViewPager pager; - private PagerIndicatorView pageIndicator; - private MediaplayerInfoPagerAdapter pagerAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - supportPostponeEnterTransition(); - } - - @Override - protected void onStop() { - super.onStop(); - Log.d(TAG, "onStop()"); - saveCurrentFragment(); - } - - @Override - public void onDestroy() { - Log.d(TAG, "onDestroy()"); - super.onDestroy(); - // don't risk creating memory leaks - drawerLayout = null; - navDrawer = null; - drawerToggle = null; - pager = null; - pagerAdapter = null; - } - - @Override - protected void chooseTheme() { - setTheme(UserPreferences.getNoTitleTheme()); - } - - void saveCurrentFragment() { - if (pager == null) { - return; - } - Log.d(TAG, "Saving preferences"); - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - prefs.edit() - .putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, pager.getCurrentItem()) - .apply(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (drawerToggle != null) { - drawerToggle.onConfigurationChanged(newConfig); - } - } - - private void loadLastFragment() { - Log.d(TAG, "Restoring instance state"); - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - int lastPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); - pager.setCurrentItem(lastPosition); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } - - @Override - protected void onAwaitingVideoSurface() { - Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); - startActivity(new Intent(this, VideoplayerActivity.class)); - } - - @Override - protected void postStatusMsg(int resId, boolean showToast) { - if (resId == R.string.player_preparing_msg - || resId == R.string.player_seeking_msg - || resId == R.string.player_buffering_msg) { - // TODO Show progress bar here - } - if (showToast) { - Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); - } - } - - @Override - protected void clearStatusMsg() { - // TODO Hide progress bar here - } - - - @Override - protected void setupGUI() { - super.setupGUI(); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(""); - drawerLayout = findViewById(R.id.drawer_layout); - navDrawer = findViewById(R.id.navDrawerFragment); - butPlaybackSpeed = findViewById(R.id.butPlaybackSpeed); - txtvPlaybackSpeed = findViewById(R.id.txtvPlaybackSpeed); - - drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close); - drawerToggle.setDrawerIndicatorEnabled(false); - drawerLayout.addDrawerListener(drawerToggle); - drawerToggle.syncState(); - - getSupportFragmentManager().beginTransaction() - .replace(R.id.navDrawerFragment, new NavDrawerFragment(), NavDrawerFragment.TAG) - .commit(); - - pager = findViewById(R.id.pager); - pager.setOffscreenPageLimit(3); - pagerAdapter = new MediaplayerInfoPagerAdapter(getSupportFragmentManager()); - pager.setAdapter(pagerAdapter); - pageIndicator = findViewById(R.id.page_indicator); - pageIndicator.setViewPager(pager); - pageIndicator.setOnClickListener(v - -> pager.setCurrentItem((pager.getCurrentItem() + 1) % pager.getChildCount())); - loadLastFragment(); - pager.onSaveInstanceState(); - - pager.post(this::supportStartPostponedEnterTransition); - } - - @Override - boolean loadMediaInfo() { - if (controller != null && controller.getMedia() != null) { - List chapters = controller.getMedia().getChapters(); - boolean hasChapters = chapters != null && !chapters.isEmpty(); - pageIndicator.setDisabledPage(hasChapters ? -1 : 2); - } - return super.loadMediaInfo(); - } - - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { - Log.d(TAG, "ReloadNotification received, switching to Videoplayer now"); - finish(); - startActivity(new Intent(this, VideoplayerActivity.class)); - - } - } - - @Override - protected void onBufferStart() { - postStatusMsg(R.string.player_buffering_msg, false); - } - - @Override - protected void onBufferEnd() { - clearStatusMsg(); - } - - public PlaybackController getPlaybackController() { - return controller; - } - - public boolean isDrawerOpen() { - return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer); - } - - @Override - protected int getContentViewResourceId() { - return R.layout.mediaplayerinfo_activity; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return (drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) || super.onOptionsItemSelected(item); - } - - @Override - public void onBackPressed() { - if (isDrawerOpen()) { - drawerLayout.closeDrawer(navDrawer); - } else if (pager == null || pager.getCurrentItem() == 0) { - // If the user is currently looking at the first step, allow the system to handle the - // Back button. This calls finish() on this activity and pops the back stack. - super.onBackPressed(); - } else { - // Otherwise, select the previous step. - pager.setCurrentItem(pager.getCurrentItem() - 1); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(MessageEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - View parentLayout = findViewById(R.id.drawer_layout); - Snackbar snackbar = Snackbar.make(parentLayout, event.message, Snackbar.LENGTH_SHORT); - if (event.action != null) { - snackbar.setAction(getString(R.string.undo), v -> event.action.run()); - } - snackbar.show(); - } - - private static class MediaplayerInfoPagerAdapter extends FragmentStatePagerAdapter { - private static final String TAG = "MPInfoPagerAdapter"; - - public MediaplayerInfoPagerAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public Fragment getItem(int position) { - Log.d(TAG, "getItem(" + position + ")"); - switch (position) { - case POS_COVER: - return new CoverFragment(); - case POS_DESCR: - return new ItemDescriptionFragment(); - case POS_CHAPTERS: - return new ChaptersFragment(); - default: - return null; - } - } - - @Override - public int getCount() { - return NUM_CONTENT_FRAGMENTS; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index a1a4e0374..212cb2f75 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -171,20 +171,6 @@ public class VideoplayerActivity extends MediaplayerActivity { } } - @Override - protected void postStatusMsg(int resId, boolean showToast) { - if (resId == R.string.player_preparing_msg) { - progressIndicator.setVisibility(View.VISIBLE); - } else { - progressIndicator.setVisibility(View.INVISIBLE); - } - } - - @Override - protected void clearStatusMsg() { - progressIndicator.setVisibility(View.INVISIBLE); - } - private final View.OnTouchListener onVideoviewTouched = (v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (PictureInPictureUtil.isInPictureInPictureMode(this)) { @@ -292,16 +278,11 @@ public class VideoplayerActivity extends MediaplayerActivity { } return; } - if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { - Log.d(TAG, "ReloadNotification received, switching to Audioplayer now"); - destroyingDueToReload = true; - finish(); - startActivity(new Intent(this, AudioplayerActivity.class)); - } else if (notificationCode == PlaybackService.EXTRA_CODE_CAST) { + if (notificationCode == PlaybackService.EXTRA_CODE_CAST) { Log.d(TAG, "ReloadNotification received, switching to Castplayer now"); destroyingDueToReload = true; finish(); - startActivity(new Intent(this, CastplayerActivity.class)); + startActivity(new Intent(this, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java index 1171acaa5..cb72a9150 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java @@ -5,37 +5,34 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; -import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ThemeUtils; import de.danoeh.antennapod.core.util.playback.Playable; -public class ChaptersListAdapter extends ArrayAdapter { - private static final String TAG = "ChapterListAdapter"; - +public class ChaptersListAdapter extends RecyclerView.Adapter { private Playable media; private final Callback callback; + private final Context context; private int currentChapterIndex = -1; private boolean hasImages = false; - public ChaptersListAdapter(Context context, int textViewResourceId, Callback callback) { - super(context, textViewResourceId); + public ChaptersListAdapter(Context context, Callback callback) { this.callback = callback; + this.context = context; } public void setMedia(Playable media) { @@ -51,34 +48,10 @@ public class ChaptersListAdapter extends ArrayAdapter { notifyDataSetChanged(); } - @NonNull + @Override - public View getView(final int position, View convertView, @NonNull ViewGroup parent) { - Holder holder; - + public void onBindViewHolder(@NonNull ChapterHolder holder, int position) { Chapter sc = getItem(position); - - // Inflate Layout - if (convertView == null) { - holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - convertView = inflater.inflate(R.layout.simplechapter_item, parent, false); - holder.view = convertView; - holder.title = convertView.findViewById(R.id.txtvTitle); - holder.start = convertView.findViewById(R.id.txtvStart); - holder.link = convertView.findViewById(R.id.txtvLink); - holder.image = convertView.findViewById(R.id.imgvCover); - holder.duration = convertView.findViewById(R.id.txtvDuration); - holder.secondaryActionButton = convertView.findViewById(R.id.secondaryActionButton); - holder.secondaryActionIcon = convertView.findViewById(R.id.secondaryActionIcon); - convertView.setTag(holder); - } else { - holder = (Holder) convertView.getTag(); - - } - holder.title.setText(sc.getTitle()); holder.start.setText(Converter.getDurationStringLong((int) sc .getStart())); @@ -89,7 +62,7 @@ public class ChaptersListAdapter extends ArrayAdapter { } else { duration = media.getDuration() - sc.getStart(); } - holder.duration.setText(getContext().getString(R.string.chapter_duration, + holder.duration.setText(context.getString(R.string.chapter_duration, Converter.getDurationStringLong((int) duration))); if (sc.getLink() == null) { @@ -97,9 +70,9 @@ public class ChaptersListAdapter extends ArrayAdapter { } else { holder.link.setVisibility(View.VISIBLE); holder.link.setText(sc.getLink()); - holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), sc.getLink())); + holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(context, sc.getLink())); } - holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(getContext(), R.attr.av_play)); + holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.av_play)); holder.secondaryActionButton.setOnClickListener(v -> { if (callback != null) { callback.onPlayChapterButtonClicked(position); @@ -107,46 +80,40 @@ public class ChaptersListAdapter extends ArrayAdapter { }); if (position == currentChapterIndex) { - int playingBackGroundColor = ThemeUtils.getColorFromAttr(getContext(), R.attr.currently_playing_background); - holder.view.setBackgroundColor(playingBackGroundColor); + int playingBackGroundColor = ThemeUtils.getColorFromAttr(context, R.attr.currently_playing_background); + holder.itemView.setBackgroundColor(playingBackGroundColor); } else { - holder.view.setBackgroundColor(ContextCompat.getColor(getContext(), android.R.color.transparent)); + holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); } if (hasImages) { holder.image.setVisibility(View.VISIBLE); if (TextUtils.isEmpty(sc.getImageUrl())) { - Glide.with(getContext()).clear(holder.image); + Glide.with(context).clear(holder.image); } else { - Glide.with(getContext()) + Glide.with(context) .load(EmbeddedChapterImage.getModelFor(media, position)) .apply(new RequestOptions() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .dontAnimate() .transforms(new FitCenter(), new RoundedCorners((int) - (4 * getContext().getResources().getDisplayMetrics().density)))) + (4 * context.getResources().getDisplayMetrics().density)))) .into(holder.image); } } else { holder.image.setVisibility(View.GONE); } - - return convertView; } - static class Holder { - View view; - TextView title; - TextView start; - TextView link; - TextView duration; - ImageView image; - View secondaryActionButton; - ImageView secondaryActionIcon; + @NonNull + @Override + public ChapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(context); + return new ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false)); } @Override - public int getCount() { + public int getItemCount() { if (media == null || media.getChapters() == null) { return 0; } @@ -160,6 +127,27 @@ public class ChaptersListAdapter extends ArrayAdapter { return counter; } + static class ChapterHolder extends RecyclerView.ViewHolder { + final TextView title; + final TextView start; + final TextView link; + final TextView duration; + final ImageView image; + final View secondaryActionButton; + final ImageView secondaryActionIcon; + + public ChapterHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.txtvTitle); + start = itemView.findViewById(R.id.txtvStart); + link = itemView.findViewById(R.id.txtvLink); + image = itemView.findViewById(R.id.imgvCover); + duration = itemView.findViewById(R.id.txtvDuration); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + } + } + public void notifyChapterChanged(int newChapterIndex) { currentChapterIndex = newChapterIndex; notifyDataSetChanged(); @@ -169,7 +157,6 @@ public class ChaptersListAdapter extends ArrayAdapter { return media.getDuration() > 0 && media.getDuration() < c.getStart(); } - @Override public Chapter getItem(int position) { int i = 0; for (Chapter chapter : media.getChapters()) { @@ -181,7 +168,7 @@ public class ChaptersListAdapter extends ArrayAdapter { } } } - return super.getItem(position); + return null; } public interface Callback { diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java index c3f5d898c..09f1af7d7 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java @@ -4,27 +4,22 @@ import android.content.Context; import android.content.Intent; import android.os.Build; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; -import de.danoeh.antennapod.activity.CastplayerActivity; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.VideoplayerActivity; import de.danoeh.antennapod.core.PlaybackServiceCallbacks; import de.danoeh.antennapod.core.feed.MediaType; - public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { @Override public Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback) { - if (remotePlayback) { - return new Intent(context, CastplayerActivity.class); - } - if (mediaType == MediaType.VIDEO) { + if (mediaType == MediaType.AUDIO || remotePlayback) { + return new Intent(context, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true); + } else { Intent i = new Intent(context, VideoplayerActivity.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); } return i; - } else { - return new Intent(context, AudioplayerActivity.class); } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index 9514ea5eb..40b8d5b84 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -313,7 +313,7 @@ public class EpisodesApplyActionFragment extends Fragment { } } if (resId != 0) { - Snackbar.make(getActivity().findViewById(R.id.content), resId, Snackbar.LENGTH_SHORT) + Snackbar.make(getActivity().findViewById(android.R.id.content), resId, Snackbar.LENGTH_SHORT) .show(); return true; } else { @@ -469,7 +469,7 @@ public class EpisodesApplyActionFragment extends Fragment { private void close(@PluralsRes int msgId, int numItems) { if (numItems > 0) { - Snackbar.make(getActivity().findViewById(R.id.content), + Snackbar.make(getActivity().findViewById(android.R.id.content), getResources().getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG ) diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java new file mode 100644 index 000000000..7bb8f5ad6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * Shows the dialog that allows setting the skip time. + */ +public class SkipPreferenceDialog { + public static void showSkipPreference(Context context, SkipDirection direction, TextView textView) { + int checked = 0; + + int skipSecs; + if (direction == SkipDirection.SKIP_FORWARD) { + skipSecs = UserPreferences.getFastForwardSecs(); + } else { + skipSecs = UserPreferences.getRewindSecs(); + } + + final int[] values = context.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] = values[i] + " " + context.getString(R.string.time_seconds); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(direction == SkipDirection.SKIP_FORWARD ? R.string.pref_fast_forward : R.string.pref_rewind); + 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 { + int seconds = values[choice]; + if (direction == SkipDirection.SKIP_FORWARD) { + UserPreferences.setFastForwardSecs(seconds); + } else { + UserPreferences.setRewindSecs(seconds); + } + if (textView != null) { + textView.setText(String.valueOf(seconds)); + } + } + }); + builder.create().show(); + } + + public enum SkipDirection { + SKIP_FORWARD, SKIP_REWIND + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java new file mode 100644 index 000000000..ed136af3c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -0,0 +1,505 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.CastEnabledActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.event.FavoritesEvent; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.TimeSpeedConverter; +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.dialog.PlaybackControlsDialog; +import de.danoeh.antennapod.dialog.SkipPreferenceDialog; +import de.danoeh.antennapod.dialog.SleepTimerDialog; +import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.view.PagerIndicatorView; +import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.text.DecimalFormat; +import java.util.List; + +/** + * Shows the audio player. + */ +public class AudioPlayerFragment extends Fragment implements + SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { + public static final String TAG = "AudioPlayerFragment"; + private static final int POS_COVER = 0; + private static final int POS_DESCR = 1; + private static final int POS_CHAPTERS = 2; + private static final int NUM_CONTENT_FRAGMENTS = 3; + private static final String PREFS = "AudioPlayerFragmentPreferences"; + private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; + private static final float EPSILON = 0.001f; + + PlaybackSpeedIndicatorView butPlaybackSpeed; + TextView txtvPlaybackSpeed; + private ViewPager pager; + private PagerIndicatorView pageIndicator; + private TextView txtvPosition; + private TextView txtvLength; + private SeekBar sbPosition; + private ImageButton butRev; + private TextView txtvRev; + private ImageButton butPlay; + private ImageButton butFF; + private TextView txtvFF; + private ImageButton butSkip; + private Toolbar toolbar; + private ProgressBar progressIndicator; + + private PlaybackController controller; + private boolean showTimeLeft; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.audioplayer_fragment, container, false); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setTitle(""); + toolbar.setNavigationOnClickListener(v -> + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); + toolbar.setOnMenuItemClickListener(this); + setupOptionsMenu(); + + ExternalPlayerFragment externalPlayerFragment = new ExternalPlayerFragment(); + getFragmentManager().beginTransaction() + .replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG) + .commit(); + + butPlaybackSpeed = root.findViewById(R.id.butPlaybackSpeed); + txtvPlaybackSpeed = root.findViewById(R.id.txtvPlaybackSpeed); + sbPosition = root.findViewById(R.id.sbPosition); + txtvPosition = root.findViewById(R.id.txtvPosition); + txtvLength = root.findViewById(R.id.txtvLength); + butRev = root.findViewById(R.id.butRev); + txtvRev = root.findViewById(R.id.txtvRev); + butPlay = root.findViewById(R.id.butPlay); + butFF = root.findViewById(R.id.butFF); + txtvFF = root.findViewById(R.id.txtvFF); + butSkip = root.findViewById(R.id.butSkip); + progressIndicator = root.findViewById(R.id.progLoading); + + setupLengthTextView(); + setupControlButtons(); + setupPlaybackSpeedButton(); + txtvRev.setText(String.valueOf(UserPreferences.getRewindSecs())); + txtvFF.setText(String.valueOf(UserPreferences.getFastForwardSecs())); + sbPosition.setOnSeekBarChangeListener(this); + + pager = root.findViewById(R.id.pager); + AudioPlayerPagerAdapter pagerAdapter = new AudioPlayerPagerAdapter(getFragmentManager()); + pager.setAdapter(pagerAdapter); + // Required for getChildAt(int) in ViewPagerBottomSheetBehavior to return the correct page + pager.setOffscreenPageLimit(NUM_CONTENT_FRAGMENTS); + pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + pager.post(() -> ((MainActivity) getActivity()).getBottomSheet().updateScrollingChild()); + } + }); + pageIndicator = root.findViewById(R.id.page_indicator); + pageIndicator.setViewPager(pager); + pageIndicator.setOnClickListener(v -> + pager.setCurrentItem((pager.getCurrentItem() + 1) % pager.getChildCount())); + return root; + } + + public View getExternalPlayerHolder() { + return getView().findViewById(R.id.playerFragment); + } + + private void setupControlButtons() { + butRev.setOnClickListener(v -> { + if (controller != null) { + int curr = controller.getPosition(); + controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); + } + }); + butRev.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(getContext(), + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev); + return true; + }); + butPlay.setOnClickListener(v -> { + if (controller != null) { + controller.init(); + controller.playPause(); + } + }); + butFF.setOnClickListener(v -> { + if (controller != null) { + int curr = controller.getPosition(); + controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); + } + }); + butFF.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(getContext(), + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF); + return false; + }); + butSkip.setOnClickListener(v -> + IntentUtils.sendLocalBroadcast(getActivity(), PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + } + + private void setupLengthTextView() { + SharedPreferences prefs = getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + txtvLength.setOnClickListener(v -> { + if (controller == null) { + return; + } + showTimeLeft = !showTimeLeft; + prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft).apply(); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + }); + } + + private void setupPlaybackSpeedButton() { + butPlaybackSpeed.setOnClickListener(v -> { + if (controller == null) { + return; + } + if (!controller.canSetPlaybackSpeed()) { + VariableSpeedDialog.showGetPluginDialog(getContext()); + return; + } + float[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); + float currentSpeed = controller.getCurrentPlaybackSpeedMultiplier(); + + int newSpeedIndex = 0; + while (newSpeedIndex < availableSpeeds.length && availableSpeeds[newSpeedIndex] < currentSpeed + EPSILON) { + newSpeedIndex++; + } + + float newSpeed; + if (availableSpeeds.length == 0) { + newSpeed = 1.0f; + } else if (newSpeedIndex == availableSpeeds.length) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = availableSpeeds[newSpeedIndex]; + } + + PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(newSpeed); + UserPreferences.setPlaybackSpeed(newSpeed); + controller.setPlaybackSpeed(newSpeed); + updateUi(); + }); + butPlaybackSpeed.setOnLongClickListener(v -> { + VariableSpeedDialog.showDialog(getContext()); + return true; + }); + butPlaybackSpeed.setVisibility(View.VISIBLE); + txtvPlaybackSpeed.setVisibility(View.VISIBLE); + } + + protected void updatePlaybackSpeedButton() { + if (butPlaybackSpeed == null || controller == null) { + return; + } + float speed = 1.0f; + if (controller.canSetPlaybackSpeed()) { + speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(controller.getMedia()); + } + String speedStr = new DecimalFormat("0.00").format(speed); + txtvPlaybackSpeed.setText(speedStr); + butPlaybackSpeed.setSpeed(speed); + butPlaybackSpeed.setAlpha(controller.canSetPlaybackSpeed() ? 1.0f : 0.5f); + butPlaybackSpeed.setVisibility(View.VISIBLE); + txtvPlaybackSpeed.setVisibility(View.VISIBLE); + } + + private PlaybackController newPlaybackController() { + return new PlaybackController(getActivity(), false) { + + @Override + public void setupGUI() { + updateUi(); + } + + @Override + public void onBufferStart() { + progressIndicator.setVisibility(View.VISIBLE); + } + + @Override + public void onBufferEnd() { + progressIndicator.setVisibility(View.GONE); + } + + @Override + public void onBufferUpdate(float progress) { + sbPosition.setSecondaryProgress((int) (progress * sbPosition.getMax())); + } + + @Override + public void handleError(int code) { + final AlertDialog.Builder errorDialog = new AlertDialog.Builder(getContext()); + errorDialog.setTitle(R.string.error_label); + errorDialog.setMessage(MediaPlayerError.getErrorString(getContext(), code)); + errorDialog.setNeutralButton(android.R.string.ok, + (dialog, which) -> { + dialog.dismiss(); + ((MainActivity) getActivity()).getBottomSheet() + .setState(BottomSheetBehavior.STATE_COLLAPSED); + } + ); + errorDialog.create().show(); + } + + @Override + public void onSleepTimerUpdate() { + setupOptionsMenu(); + } + + @Override + public ImageButton getPlayButton() { + return butPlay; + } + + @Override + public boolean loadMediaInfo() { + updateUi(); + return true; + } + + @Override + public void onShutdownNotification() { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + + @Override + public void onPlaybackEnd() { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + + @Override + public void onPlaybackSpeedChange() { + updatePlaybackSpeedButton(); + } + + @Override + public void onSetSpeedAbilityChanged() { + updatePlaybackSpeedButton(); + } + }; + } + + private void updateUi() { + if (controller == null) { + return; + } + updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + updatePlaybackSpeedButton(); + setupOptionsMenu(); + + if (controller.getMedia() != null) { + List chapters = controller.getMedia().getChapters(); + boolean hasChapters = chapters != null && !chapters.isEmpty(); + pageIndicator.setDisabledPage(hasChapters ? -1 : 2); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onStart() { + super.onStart(); + controller = newPlaybackController(); + controller.init(); + updateUi(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void updatePosition(PlaybackPositionEvent event) { + if (controller == null || txtvPosition == null || txtvLength == null || sbPosition == null) { + return; + } + + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int currentPosition = converter.convert(event.getPosition()); + int duration = converter.convert(event.getDuration()); + int remainingTime = converter.convert(event.getDuration() - event.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)); + } + float progress = ((float) event.getPosition()) / event.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void favoritesChanged(FavoritesEvent event) { + setupOptionsMenu(); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (controller == null || txtvLength == null) { + return; + } + if (fromUser) { + float prog = progress / ((float) seekBar.getMax()); + int duration = controller.getDuration(); + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int position = converter.convert((int) (prog * duration)); + txtvPosition.setText(Converter.getDurationStringLong(position)); + + if (showTimeLeft && prog != 0) { + int timeLeft = converter.convert(duration - (int) (prog * duration)); + String length = "-" + Converter.getDurationStringLong(timeLeft); + txtvLength.setText(length); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // interrupt position Observer, restart later + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (controller != null) { + float prog = seekBar.getProgress() / ((float) seekBar.getMax()); + controller.seekTo((int) (prog * controller.getDuration())); + } + } + + public void setupOptionsMenu() { + if (toolbar.getMenu().size() == 0) { + toolbar.inflateMenu(R.menu.mediaplayer); + } + if (controller == null) { + return; + } + Playable media = controller.getMedia(); + boolean isFeedMedia = media instanceof FeedMedia; + toolbar.getMenu().findItem(R.id.open_feed_item).setVisible(isFeedMedia); + if (isFeedMedia) { + FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), ((FeedMedia) media).getItem()); + } + + toolbar.getMenu().findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); + toolbar.getMenu().findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); + + ((CastEnabledActivity) getActivity()).requestCastButton(toolbar.getMenu()); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (controller == null) { + return false; + } + Playable media = controller.getMedia(); + if (media == null) { + return false; + } + + final @Nullable FeedItem feedItem = (media instanceof FeedMedia) ? ((FeedMedia) media).getItem() : null; + if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), feedItem)) { + return true; + } + switch (item.getItemId()) { + case R.id.disable_sleeptimer_item: // Fall-through + case R.id.set_sleeptimer_item: + new SleepTimerDialog().show(getFragmentManager(), "SleepTimerDialog"); + return true; + case R.id.audio_controls: + PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(false); + dialog.show(getFragmentManager(), "playback_controls"); + return true; + case R.id.open_feed_item: + if (feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(getContext(), feedItem.getFeedId()); + startActivity(intent); + } + return true; + } + return false; + } + + private static class AudioPlayerPagerAdapter extends FragmentPagerAdapter { + private static final String TAG = "AudioPlayerPagerAdapter"; + + public AudioPlayerPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + Log.d(TAG, "getItem(" + position + ")"); + switch (position) { + case POS_COVER: + return new CoverFragment(); + case POS_DESCR: + return new ItemDescriptionFragment(); + case POS_CHAPTERS: + return new ChaptersFragment(); + default: + return null; + } + } + + @Override + public int getCount() { + return NUM_CONTENT_FRAGMENTS; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index 9940ccbdd..0aba568d1 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -1,24 +1,24 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; -import androidx.fragment.app.ListFragment; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; -import android.widget.ListView; - -import de.danoeh.antennapod.core.util.ChapterUtils; -import java.util.List; -import java.util.ListIterator; - +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChaptersListAdapter; -import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; - import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -28,38 +28,43 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -public class ChaptersFragment extends ListFragment { +public class ChaptersFragment extends Fragment { private static final String TAG = "ChaptersFragment"; private ChaptersListAdapter adapter; private PlaybackController controller; private Disposable disposable; private int focusedChapter = -1; private Playable media; + private LinearLayoutManager layoutManager; + @Nullable @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - // add padding - final ListView lv = getListView(); - lv.setClipToPadding(false); - final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); - lv.setPadding(0, vertPadding, 0, vertPadding); + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.simple_list_fragment, container, false); + root.findViewById(R.id.toolbar).setVisibility(View.GONE); + RecyclerView recyclerView = root.findViewById(R.id.recyclerView); + layoutManager = new LinearLayoutManager(getActivity()); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + + adapter = new ChaptersListAdapter(getActivity(), pos -> { + if (controller.getStatus() != PlayerStatus.PLAYING) { + controller.playPause(); + } + Chapter chapter = adapter.getItem(pos); + controller.seekToChapter(chapter); + updateChapterSelection(pos); + }); + recyclerView.setAdapter(adapter); EmptyViewHandler emptyView = new EmptyViewHandler(getContext()); - emptyView.attachToListView(lv); + emptyView.attachToRecyclerView(recyclerView); emptyView.setIcon(R.attr.ic_bookmark); emptyView.setTitle(R.string.no_chapters_head_label); emptyView.setMessage(R.string.no_chapters_label); - adapter = new ChaptersListAdapter(getActivity(), 0, pos -> { - if (controller.getStatus() != PlayerStatus.PLAYING) { - controller.playPause(); - } - Chapter chapter = (Chapter) getListAdapter().getItem(pos); - controller.seekToChapter(chapter); - updateChapterSelection(pos); - }); - setListAdapter(adapter); + return root; } @Override @@ -136,7 +141,6 @@ public class ChaptersFragment extends ListFragment { return; } adapter.setMedia(media); - adapter.notifyDataSetChanged(); int positionOfCurrentChapter = getCurrentChapter(media); updateChapterSelection(positionOfCurrentChapter); } @@ -149,9 +153,9 @@ public class ChaptersFragment extends ListFragment { if (position != -1 && focusedChapter != position) { focusedChapter = position; adapter.notifyChapterChanged(focusedChapter); - if (getListView().getFirstVisiblePosition() >= position - || getListView().getLastVisiblePosition() <= position) { - getListView().setSelectionFromTop(position, 100); + if (layoutManager.findFirstCompletelyVisibleItemPosition() >= position + || layoutManager.findLastCompletelyVisibleItemPosition() <= position) { + layoutManager.scrollToPositionWithOffset(position, 100); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index ce2232a55..03b1d6f8f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -1,9 +1,7 @@ package de.danoeh.antennapod.fragment; import android.content.Intent; -import android.os.Build; import android.os.Bundle; -import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -17,7 +15,9 @@ import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; @@ -40,7 +40,6 @@ import org.greenrobot.eventbus.ThreadMode; public class ExternalPlayerFragment extends Fragment { public static final String TAG = "ExternalPlayerFragment"; - private ViewGroup fragmentLayout; private ImageView imgvCover; private TextView txtvTitle; private ImageButton butPlay; @@ -56,26 +55,21 @@ public class ExternalPlayerFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.external_player_fragment, - container, false); - fragmentLayout = root.findViewById(R.id.fragmentLayout); + View root = inflater.inflate(R.layout.external_player_fragment, container, false); imgvCover = root.findViewById(R.id.imgvCover); txtvTitle = root.findViewById(R.id.txtvTitle); butPlay = root.findViewById(R.id.butPlay); mFeedName = root.findViewById(R.id.txtvAuthor); mProgressBar = root.findViewById(R.id.episodeProgress); - fragmentLayout.setOnClickListener(v -> { + root.findViewById(R.id.fragmentLayout).setOnClickListener(v -> { Log.d(TAG, "layoutInfo was clicked"); if (controller != null && controller.getMedia() != null) { - Intent intent = PlaybackService.getPlayerActivityIntent(getActivity(), controller.getMedia()); - if (controller.getMedia().getMediaType() == MediaType.AUDIO) { - ActivityOptionsCompat options = ActivityOptionsCompat - .makeSceneTransitionAnimation(getActivity(), imgvCover, "coverTransition"); - startActivity(intent, options.toBundle()); + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_EXPANDED); } else { + Intent intent = PlaybackService.getPlayerActivityIntent(getActivity(), controller.getMedia()); startActivity(intent); } } @@ -178,9 +172,7 @@ public class ExternalPlayerFragment extends Fragment { } private void playbackDone() { - if (fragmentLayout != null) { - fragmentLayout.setVisibility(View.GONE); - } + clearUi(); if (controller != null) { controller.release(); } @@ -217,10 +209,22 @@ public class ExternalPlayerFragment extends Fragment { .observeOn(AndroidSchedulers.mainThread()) .subscribe(media -> updateUi((Playable) media), error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> fragmentLayout.setVisibility(View.GONE)); + this::clearUi); return true; } + private void clearUi() { + if (txtvTitle == null || mFeedName == null || mProgressBar == null || butPlay == null) { + return; + } + txtvTitle.setText(R.string.no_media_playing_label); + mFeedName.setText(""); + butPlay.setVisibility(View.GONE); + mProgressBar.setProgress(0); + Glide.with(getActivity()).clear(imgvCover); + ((MainActivity) getActivity()).getBottomSheet().setLocked(true); + } + private void updateUi(Playable media) { if (media != null) { txtvTitle.setText(media.getEpisodeTitle()); @@ -236,12 +240,13 @@ public class ExternalPlayerFragment extends Fragment { .fitCenter() .dontAnimate()) .into(imgvCover); - - fragmentLayout.setVisibility(View.VISIBLE); if (controller != null && controller.isPlayingVideoLocally()) { butPlay.setVisibility(View.GONE); + ((MainActivity) getActivity()).getBottomSheet().setLocked(true); + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); } else { butPlay.setVisibility(View.VISIBLE); + ((MainActivity) getActivity()).getBottomSheet().setLocked(false); } } else { Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 256615199..58cc9290c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -10,6 +10,7 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -35,7 +36,8 @@ public class ItemDescriptionFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "Creating view"); - webvDescription = new ShownotesWebView(getActivity().getApplicationContext()); + View root = inflater.inflate(R.layout.item_description_fragment, container, false); + webvDescription = root.findViewById(R.id.webview); webvDescription.setTimecodeSelectedListener(time -> { if (controller != null) { controller.seekTo(time); @@ -46,7 +48,7 @@ public class ItemDescriptionFragment extends Fragment { webvDescription.postDelayed(ItemDescriptionFragment.this::restoreFromPreference, 50); }); registerForContextMenu(webvDescription); - return webvDescription; + return root; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java index f251de5ec..39abdeda5 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java @@ -146,9 +146,6 @@ public class ItemPagerFragment extends Fragment { return; } super.onCreateOptionsMenu(menu, inflater); - if (Flavors.FLAVOR == Flavors.PLAY) { - ((CastEnabledActivity) getActivity()).requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS); - } inflater.inflate(R.menu.feeditem_options, menu); if (item.hasMedia()) { FeedItemMenuHandler.onPrepareMenu(menu, item); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 0a752b855..0411fd01b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -416,11 +416,11 @@ public class QueueFragment extends Fragment { recyclerAdapter.setLocked(locked); } if (locked) { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_locked, Snackbar.LENGTH_SHORT).show(); + Snackbar.make(getActivity().findViewById(android.R.id.content), + R.string.queue_locked, Snackbar.LENGTH_SHORT).show(); } else { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_unlocked, Snackbar.LENGTH_SHORT).show(); + Snackbar.make(getActivity().findViewById(android.R.id.content), + R.string.queue_unlocked, Snackbar.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java index 34684ac49..6b2255b52 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java @@ -9,11 +9,11 @@ import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MediaplayerActivity; import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; +import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper; import java.util.Map; @@ -48,11 +48,11 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat { return true; }); findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { - MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_REWIND); + SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); return true; }); findPreference(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { - MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_FORWARD); + SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); return true; }); if (!PictureInPictureUtil.supportsPictureInPicture(activity)) { diff --git a/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java b/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java new file mode 100644 index 000000000..8e8d98fc9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.ViewPagerBottomSheetBehavior; + +/** + * Based on https://stackoverflow.com/a/40798214 + */ +public class LockableBottomSheetBehavior extends ViewPagerBottomSheetBehavior { + private boolean isLocked = false; + + public LockableBottomSheetBehavior() {} + + public LockableBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setLocked(boolean locked) { + isLocked = locked; + } + + @Override + public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + boolean handled = false; + + if (!isLocked) { + handled = super.onInterceptTouchEvent(parent, child, event); + } + + return handled; + } + + @Override + public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + boolean handled = false; + + if (!isLocked) { + handled = super.onTouchEvent(parent, child, event); + } + + return handled; + } + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, + View target, int nestedScrollAxes) { + boolean handled = false; + + if (!isLocked) { + handled = super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); + } + + return handled; + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, + int dx, int dy, int[] consumed) { + if (!isLocked) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); + } + } + + @Override + public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { + if (!isLocked) { + super.onStopNestedScroll(coordinatorLayout, child, target); + } + } + + @Override + public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, + float velocityX, float velocityY) { + boolean handled = false; + + if (!isLocked) { + handled = super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + return handled; + } +} diff --git a/app/src/main/res/drawable/shadow.xml b/app/src/main/res/drawable/shadow.xml deleted file mode 100644 index fc5110e0b..000000000 --- a/app/src/main/res/drawable/shadow.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/layout/mediaplayerinfo_activity.xml b/app/src/main/res/layout/audioplayer_fragment.xml similarity index 82% rename from app/src/main/res/layout/mediaplayerinfo_activity.xml rename to app/src/main/res/layout/audioplayer_fragment.xml index 526994752..905371fa4 100644 --- a/app/src/main/res/layout/mediaplayerinfo_activity.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -1,24 +1,21 @@ - - - - - - + + + android:layout_marginBottom="12dp"/> - + tools:background="@android:color/holo_green_dark"/> - - - + tools:background="@android:color/holo_green_dark"/> - + android:background="?android:attr/selectableItemBackground" + tools:background="@android:color/holo_green_dark"/> - + - - + tools:background="@android:color/holo_green_dark"/> - + android:layout_centerVertical="true"/> - + + + tools:background="@android:color/holo_blue_dark"/> - + android:clickable="false"/> - + tools:background="@android:color/holo_green_dark"/> - + android:clickable="false"/> - + tools:background="@android:color/holo_blue_dark"/> - + android:clickable="false"/> - - + tools:background="@android:color/holo_green_dark"/> + - + - - - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml index 3e2efe47e..651953b0e 100644 --- a/app/src/main/res/layout/external_player_fragment.xml +++ b/app/src/main/res/layout/external_player_fragment.xml @@ -1,87 +1,84 @@ - - - + android:background="?attr/selectableItemBackground" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + android:id="@+id/episodeProgress" + android:layout_width="match_parent" + android:layout_height="4dp" + style="?attr/progressBarTheme" + android:indeterminate="false" + tools:progress="100"/> - - - - - - - + diff --git a/app/src/main/res/layout/item_description_fragment.xml b/app/src/main/res/layout/item_description_fragment.xml new file mode 100644 index 000000000..96382eae3 --- /dev/null +++ b/app/src/main/res/layout/item_description_fragment.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index 89b7e0c47..60b9623bc 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -1,42 +1,43 @@ - - - + android:layout_height="match_parent"> - + + + + android:id="@+id/navDrawerFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginEnd="24dp" + android:layout_marginRight="24dp" + android:layout_gravity="start" + android:orientation="vertical" /> \ No newline at end of file diff --git a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java index e392a50c6..10678f556 100644 --- a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java +++ b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java @@ -4,8 +4,6 @@ import android.content.SharedPreferences; import android.media.AudioManager; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.CallSuper; -import androidx.core.view.MenuItemCompat; import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.Menu; @@ -14,6 +12,7 @@ import android.view.MenuItem; import com.google.android.gms.cast.ApplicationMetadata; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.cast.CastButtonVisibilityManager; import de.danoeh.antennapod.core.cast.CastConsumer; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.DefaultCastConsumer; @@ -21,6 +20,9 @@ import de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import java.util.ArrayList; +import java.util.List; + /** * Activity that allows for showing the MediaRouter button whenever there's a cast device in the * network. @@ -31,9 +33,7 @@ public abstract class CastEnabledActivity extends AppCompatActivity private CastConsumer castConsumer; private CastManager castManager; - - private SwitchableMediaRouteActionProvider mediaRouteActionProvider; - private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(); + private final List castButtons = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,8 +59,10 @@ public abstract class CastEnabledActivity extends AppCompatActivity }; castManager = CastManager.getInstance(); castManager.addCastConsumer(castConsumer); + CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(castManager); castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled()); onCastConnectionChanged(castManager.isConnected()); + castButtons.add(castButtonVisibilityManager); } @Override @@ -75,46 +77,15 @@ public abstract class CastEnabledActivity extends AppCompatActivity super.onDestroy(); } - @Override - @CallSuper - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - if (!CastManager.isInitialized()) { - return true; - } - getMenuInflater().inflate(R.menu.cast_enabled, menu); - castButtonVisibilityManager.setMenu(menu); - return true; - } - - @Override - @CallSuper - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - if (!CastManager.isInitialized()) { - return true; - } - - MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item); - if (mediaRouteButton == null) { - Log.wtf(TAG, "MediaRoute item could not be found on the menu!", new Exception()); - mediaRouteActionProvider = null; - return true; - } - mediaRouteActionProvider = castManager.addMediaRouterButton(mediaRouteButton); - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable()); - } - return true; - } - @Override protected void onResume() { super.onResume(); if (!CastManager.isInitialized()) { return; } - castButtonVisibilityManager.setResumed(true); + for (CastButtonVisibilityManager castButton : castButtons) { + castButton.setResumed(true); + } } @Override @@ -123,7 +94,9 @@ public abstract class CastEnabledActivity extends AppCompatActivity if (!CastManager.isInitialized()) { return; } - castButtonVisibilityManager.setResumed(false); + for (CastButtonVisibilityManager castButton : castButtons) { + castButton.setResumed(false); + } } @@ -132,7 +105,9 @@ public abstract class CastEnabledActivity extends AppCompatActivity if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { boolean newValue = UserPreferences.isCastEnabled(); Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue); - castButtonVisibilityManager.setPrefEnabled(newValue); + for (CastButtonVisibilityManager castButton : castButtons) { + castButton.setPrefEnabled(newValue); + } // PlaybackService has its own listener, so if it's active we don't have to take action here. if (!newValue && !PlaybackService.isRunning) { CastManager.getInstance().disconnect(); @@ -142,129 +117,41 @@ public abstract class CastEnabledActivity extends AppCompatActivity private void onCastConnectionChanged(boolean connected) { if (connected) { - castButtonVisibilityManager.onConnected(); + for (CastButtonVisibilityManager castButton : castButtons) { + castButton.onConnected(); + } setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); } else { - castButtonVisibilityManager.onDisconnected(); + for (CastButtonVisibilityManager castButton : castButtons) { + castButton.onDisconnected(); + } setVolumeControlStream(AudioManager.STREAM_MUSIC); } } /** * Should be called by any activity or fragment for which the cast button should be shown. - * - * @param showAsAction refer to {@link MenuItem#setShowAsAction(int)} */ - public final void requestCastButton(int showAsAction) { + public final void requestCastButton(Menu menu) { if (!CastManager.isInitialized()) { return; } - castButtonVisibilityManager.requestCastButton(showAsAction); - } - private class CastButtonVisibilityManager { - private volatile boolean prefEnabled = false; - private volatile boolean viewRequested = false; - private volatile boolean resumed = false; - private volatile boolean connected = false; - private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; - private Menu menu; - - public synchronized void setPrefEnabled(boolean newValue) { - if (prefEnabled != newValue && resumed && (viewRequested || connected)) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - prefEnabled = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } + MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item); + if (mediaRouteButton == null) { + getMenuInflater().inflate(R.menu.cast_enabled, menu); + mediaRouteButton = menu.findItem(R.id.media_route_menu_item); } - public synchronized void setResumed(boolean newValue) { - if (resumed == newValue) { - Log.e(TAG, "resumed should never change to the same value"); - return; - } - resumed = newValue; - if (prefEnabled && (viewRequested || connected)) { - if (resumed) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - } - - public synchronized void setViewRequested(boolean newValue) { - if (viewRequested != newValue && resumed && prefEnabled && !connected) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - viewRequested = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } - } - - public synchronized void setConnected(boolean newValue) { - if (connected != newValue && resumed && prefEnabled && !prefEnabled) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - connected = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } - } - - public synchronized boolean shouldEnable() { - return prefEnabled && viewRequested; - } - - public void setMenu(Menu menu) { - setViewRequested(false); - showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; - this.menu = menu; - setShowAsAction(); - } - - public void requestCastButton(int showAsAction) { - setViewRequested(true); - this.showAsAction = showAsAction; - setShowAsAction(); - } - - public void onConnected() { - setConnected(true); - setShowAsAction(); - } - - public void onDisconnected() { - setConnected(false); - setShowAsAction(); - } - - private void setShowAsAction() { - if (menu == null) { - Log.d(TAG, "setShowAsAction() without a menu"); - return; - } - MenuItem item = menu.findItem(R.id.media_route_menu_item); - if (item == null) { - Log.e(TAG, "setShowAsAction(), but cast button not inflated"); - return; - } - MenuItemCompat.setShowAsAction(item, connected? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction); - } + SwitchableMediaRouteActionProvider mediaRouteActionProvider = + CastManager.getInstance().addMediaRouterButton(mediaRouteButton); + CastButtonVisibilityManager castButtonVisibilityManager = + new CastButtonVisibilityManager(CastManager.getInstance()); + castButtonVisibilityManager.setMenu(menu); + castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled()); + castButtonVisibilityManager.mediaRouteActionProvider = mediaRouteActionProvider; + castButtonVisibilityManager.setResumed(true); + castButtonVisibilityManager.requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS); + mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index b92dd217d..63b6ee7e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -181,11 +181,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ public static final int NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED = 9; - /** - * Send a message to the user (with provided String resource id) - */ - public static final int NOTIFICATION_TYPE_SHOW_TOAST = 10; - /** * Returned by getPositionSafe() or getDurationSafe() if the playbackService * is in an invalid state. @@ -1218,7 +1213,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.setCasting(isCasting); notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); - Log.d(TAG, "setupNotification: startForeground" + playerStatus); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); startForegroundIfPlaying(playerStatus); @@ -1236,10 +1230,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { } private void startForegroundIfPlaying(@NonNull PlayerStatus status) { + Log.d(TAG, "startForegroundIfPlaying: " + status); if (stateManager.hasReceivedValidStartCommand()) { if (isCasting || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARING || status == PlayerStatus.SEEKING) { stateManager.startForeground(NOTIFICATION_ID, notificationBuilder.build()); + Log.d(TAG, "foreground"); } else { stateManager.stopForeground(false); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); @@ -1781,6 +1777,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void setIsCasting(boolean isCasting) { PlaybackService.isCasting = isCasting; + stateManager.validStartCommandWasReceived(); } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index a11406171..1f77d0af0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -21,6 +21,7 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; +import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.util.ThemeUtils; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -333,8 +334,6 @@ public class PlaybackController { case PlaybackService.NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED: onSetSpeedAbilityChanged(); break; - case PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST: - postStatusMsg(code, true); } } @@ -411,11 +410,10 @@ public class PlaybackController { Log.d(TAG, "status: " + status.toString()); switch (status) { case ERROR: - postStatusMsg(R.string.player_error_msg, false); + EventBus.getDefault().post(new MessageEvent(activity.getString(R.string.player_error_msg))); handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); break; case PAUSED: - clearStatusMsg(); checkMediaInfoLoaded(); onPositionObserverUpdate(); updatePlayButtonAppearance(playResource, playText); @@ -425,7 +423,6 @@ public class PlaybackController { } break; case PLAYING: - clearStatusMsg(); checkMediaInfoLoaded(); if (!PlaybackService.isCasting() && PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { @@ -435,7 +432,6 @@ public class PlaybackController { updatePlayButtonAppearance(pauseResource, pauseText); break; case PREPARING: - postStatusMsg(R.string.player_preparing_msg, false); checkMediaInfoLoaded(); if (playbackService != null) { if (playbackService.isStartWhenPrepared()) { @@ -446,21 +442,17 @@ public class PlaybackController { } break; case STOPPED: - postStatusMsg(R.string.player_stopped_msg, false); break; case PREPARED: checkMediaInfoLoaded(); - postStatusMsg(R.string.player_ready_msg, false); updatePlayButtonAppearance(playResource, playText); onPositionObserverUpdate(); break; case SEEKING: onPositionObserverUpdate(); - postStatusMsg(R.string.player_seeking_msg, false); break; case INITIALIZED: checkMediaInfoLoaded(); - clearStatusMsg(); updatePlayButtonAppearance(playResource, playText); break; } @@ -482,10 +474,6 @@ public class PlaybackController { return null; } - public void postStatusMsg(int msg, boolean showToast) {} - - public void clearStatusMsg() {} - public boolean loadMediaInfo() { return false; } @@ -599,8 +587,8 @@ public class PlaybackController { public int getPosition() { if (playbackService != null) { return playbackService.getCurrentPosition(); - } else if (media != null) { - return media.getPosition(); + } else if (getMedia() != null) { + return getMedia().getPosition(); } else { return PlaybackService.INVALID_TIME; } @@ -609,8 +597,8 @@ public class PlaybackController { public int getDuration() { if (playbackService != null) { return playbackService.getDuration(); - } else if (media != null) { - return media.getDuration(); + } else if (getMedia() != null) { + return getMedia().getDuration(); } else { return PlaybackService.INVALID_TIME; } diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index 5083eb749..cfb272ad9 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -3,7 +3,7 @@ 0dp 70dp - 56dp + 64dp 20dp 12sp 14sp diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java new file mode 100644 index 000000000..527a652e3 --- /dev/null +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.core.cast; + +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import androidx.core.view.MenuItemCompat; +import de.danoeh.antennapod.core.R; + +public class CastButtonVisibilityManager { + private static final String TAG = "CastBtnVisibilityMgr"; + private final CastManager castManager; + private volatile boolean prefEnabled = false; + private volatile boolean viewRequested = false; + private volatile boolean resumed = false; + private volatile boolean connected = false; + private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; + private Menu menu; + public SwitchableMediaRouteActionProvider mediaRouteActionProvider; + + public CastButtonVisibilityManager(CastManager castManager) { + this.castManager = castManager; + } + + public synchronized void setPrefEnabled(boolean newValue) { + if (prefEnabled != newValue && resumed && (viewRequested || connected)) { + if (newValue) { + castManager.incrementUiCounter(); + } else { + castManager.decrementUiCounter(); + } + } + prefEnabled = newValue; + if (mediaRouteActionProvider != null) { + mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); + } + } + + public synchronized void setResumed(boolean newValue) { + if (resumed == newValue) { + Log.e(TAG, "resumed should never change to the same value"); + return; + } + resumed = newValue; + if (prefEnabled && (viewRequested || connected)) { + if (resumed) { + castManager.incrementUiCounter(); + } else { + castManager.decrementUiCounter(); + } + } + } + + public synchronized void setViewRequested(boolean newValue) { + if (viewRequested != newValue && resumed && prefEnabled && !connected) { + if (newValue) { + castManager.incrementUiCounter(); + } else { + castManager.decrementUiCounter(); + } + } + viewRequested = newValue; + if (mediaRouteActionProvider != null) { + mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); + } + } + + public synchronized void setConnected(boolean newValue) { + if (connected != newValue && resumed && prefEnabled && !prefEnabled) { + if (newValue) { + castManager.incrementUiCounter(); + } else { + castManager.decrementUiCounter(); + } + } + connected = newValue; + if (mediaRouteActionProvider != null) { + mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); + } + } + + public synchronized boolean shouldEnable() { + return prefEnabled && viewRequested; + } + + public void setMenu(Menu menu) { + setViewRequested(false); + showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; + this.menu = menu; + setShowAsAction(); + } + + public void requestCastButton(int showAsAction) { + setViewRequested(true); + this.showAsAction = showAsAction; + setShowAsAction(); + } + + public void onConnected() { + setConnected(true); + setShowAsAction(); + } + + public void onDisconnected() { + setConnected(false); + setShowAsAction(); + } + + private void setShowAsAction() { + if (menu == null) { + Log.d(TAG, "setShowAsAction() without a menu"); + return; + } + MenuItem item = menu.findItem(R.id.media_route_menu_item); + if (item == null) { + Log.e(TAG, "setShowAsAction(), but cast button not inflated"); + return; + } + MenuItemCompat.setShowAsAction(item, connected ? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction); + } +} diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java index c32ba2385..31e955017 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java @@ -26,9 +26,11 @@ import java.util.concurrent.TimeoutException; import de.danoeh.antennapod.core.cast.CastConsumer; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.DefaultCastConsumer; +import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; +import org.greenrobot.eventbus.EventBus; /** * Class intended to work along PlaybackService and provide support for different flavors. @@ -106,7 +108,7 @@ public class PlaybackServiceFlavorHelper { } switch (code) { case RemotePSMP.CAST_ERROR: - callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST, resourceId); + EventBus.getDefault().post(new MessageEvent(context.getString(resourceId))); return true; case RemotePSMP.CAST_ERROR_PRIORITY_HIGH: Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();