diff options
198 files changed, 3564 insertions, 3598 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..ad6ac1f5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +# Settings in .editorconfig should match checkstyle config + +root = true + +[*] +charset = utf-8 +max_line_length = 120 @@ -32,5 +32,5 @@ If you want to translate AntennaPod into another language, you can visit the [Tr ## Building AntennaPod -Information on how to build AntennaPod can be found in the [wiki](https://github.com/AntennaPod/AntennaPod/wiki/Building-AntennaPod). +You can build AntennaPod just like any other Android project. On Android Studio, simply select `File` » `New` » `Project from version control` and paste AntennaPod's GitHub URL. diff --git a/app/build.gradle b/app/build.gradle index 6db25723c..d8dac6b63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ plugins { id('com.android.application') - id('com.github.triplet.play') version '3.7.0-agp4.2' apply false + id('com.github.triplet.play') version '3.7.0' apply false } apply from: "../common.gradle" apply from: "../playFlavor.gradle" @@ -56,23 +56,19 @@ android { } } - lintOptions { + lint { disable 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams', 'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'AllowBackup', 'VectorDrawableCompat', 'StaticFieldLeak', 'UseCompoundDrawables', 'NestedWeights', 'Overdraw', 'UselessParent', 'TextFields', 'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription', 'KeyboardInaccessibleWidget', 'LabelFor', 'SetTextI18n', 'HardcodedText', 'RelativeOverlap', 'RtlCompat', 'RtlHardcoded', 'MissingMediaBrowserServiceIntentFilter', 'VectorPath', - 'InvalidPeriodicWorkRequestInterval' + 'InvalidPeriodicWorkRequestInterval', 'NotifyDataSetChanged', 'RtlEnabled' } - aaptOptions { + androidResources { additionalParameters "--no-version-vectors" } - - dexOptions { - jumboMode true - } } dependencies { @@ -125,7 +121,6 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion" implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion" implementation 'com.leinardi.android:speed-dial:3.2.0' - implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.4.0' implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3' diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index bb3c0bf67..16605d202 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -4,44 +4,38 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.view.KeyEvent; -import androidx.preference.PreferenceManager; import android.view.View; - +import androidx.preference.PreferenceManager; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; - +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.test.antennapod.EspressoTestUtils; +import de.test.antennapod.IgnoreOnCi; +import de.test.antennapod.ui.UITestUtils; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.test.antennapod.EspressoTestUtils; -import de.test.antennapod.IgnoreOnCi; -import de.test.antennapod.ui.UITestUtils; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; @@ -68,31 +62,20 @@ import static org.junit.Assert.assertTrue; */ @LargeTest @IgnoreOnCi -@RunWith(Parameterized.class) public class PlaybackTest { @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class, false, false); - @Parameterized.Parameter(value = 0) - public String playerToUse; private UITestUtils uiTestUtils; protected Context context; private PlaybackController controller; - @Parameterized.Parameters(name = "{0}") - public static Collection<Object[]> initParameters() { - return Arrays.asList(new Object[][] { { "exoplayer" }, { "builtin" }, { "sonic" } }); - } - @Before public void setUp() throws Exception { context = InstrumentationRegistry.getInstrumentation().getTargetContext(); EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, playerToUse).apply(); - uiTestUtils = new UITestUtils(context); uiTestUtils.setup(); } @@ -252,7 +235,8 @@ public class PlaybackTest { openNavDrawer(); onDrawerItem(withText(R.string.episodes_label)).perform(click()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); + final List<FeedItem> episodes = DBReader.getEpisodes(0, 10, + FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD); Matcher<View> allEpisodesMatcher = allOf(withId(R.id.recyclerView), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton))); @@ -287,7 +271,8 @@ public class PlaybackTest { uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); activityTestRule.launchActivity(new Intent()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); + final List<FeedItem> episodes = DBReader.getEpisodes(0, 10, + FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD); startLocalPlayback(); FeedMedia media = episodes.get(0).getMedia(); diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java index aa2f1cf98..e8436a3a2 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -129,7 +129,8 @@ public class PlaybackServiceMediaPlayerTest { private Playable writeTestPlayable(String downloadUrl, String fileUrl) { Feed f = new Feed(0, null, "f", "l", "d", null, null, null, null, "i", null, null, "l", false); - FeedPreferences prefs = new FeedPreferences(f.getId(), false, FeedPreferences.AutoDeleteAction.NO, VolumeAdaptionSetting.OFF, null, null); + FeedPreferences prefs = new FeedPreferences(f.getId(), false, FeedPreferences.AutoDeleteAction.NEVER, + VolumeAdaptionSetting.OFF, FeedPreferences.NewEpisodesAction.NOTHING, null, null); f.setPreferences(prefs); f.setItems(new ArrayList<>()); FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), FeedItem.UNPLAYED, f); diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java new file mode 100644 index 000000000..4339d6cd7 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java @@ -0,0 +1,22 @@ +package de.test.antennapod.service.playback; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; + +public class SleepTimerPreferencesTest { + @Test + public void testIsInTimeRange() { + assertTrue(SleepTimerPreferences.isInTimeRange(0, 10, 8)); + assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 8)); + assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 1)); + assertTrue(SleepTimerPreferences.isInTimeRange(20, 10, 8)); + assertTrue(SleepTimerPreferences.isInTimeRange(20, 20, 8)); + assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 8)); + assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 6)); + assertFalse(SleepTimerPreferences.isInTimeRange(20, 6, 8)); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java index f3cd99b2c..861c62f1b 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java @@ -2,13 +2,11 @@ package de.test.antennapod.ui; import android.content.Intent; import androidx.test.espresso.intent.rule.IntentsTestRule; -import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedPreferences; import de.test.antennapod.EspressoTestUtils; import org.junit.After; import org.junit.Before; @@ -18,8 +16,6 @@ import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.pressBack; -import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; @@ -28,10 +24,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.waitForView; import static org.hamcrest.Matchers.allOf; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.io.IOException; @RunWith(AndroidJUnit4.class) public class FeedSettingsTest { @@ -84,52 +76,4 @@ public class FeedSettingsTest { clickPreference(R.string.feed_volume_reduction); onView(withText(R.string.cancel_label)).perform(click()); } - - /** - * Test that modifying a feed's authentication settings results in proper behavior. - * Expect: - * - Feed is refreshed automatically - * - Database has updated username and password - */ - @Test - public void testAuthenticationSettingsUpdate() throws IOException { - onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.appBar)), - withText(feed.getTitle()), isDisplayed()), 1000)); - - String updatedTitle = "modified episode title"; - String username = "username"; - String password = "password"; - - // update feed hosted on server - feed.getItems().get(0).setTitle(updatedTitle); - uiTestUtils.hostFeed(feed); - - // interact with UI to update authentication settings - updateAuthenticationSettings(username, password); - - // expect feed to have refreshed and be showing new episode title - onView(isRoot()).perform(waitForView(withText(updatedTitle), 5000)); - - // expect database to be updated with correct username and password - Feed updatedFeed = DBReader.getFeed(feed.getId()); - assertNotNull(updatedFeed); - - FeedPreferences updatedFeedPreferences = updatedFeed.getPreferences(); - assertNotNull(updatedFeedPreferences); - - assertEquals("database updated with username", username, updatedFeedPreferences.getUsername()); - assertEquals("database updated with password", password, updatedFeedPreferences.getPassword()); - } - - private void updateAuthenticationSettings(String username, String password) { - onView(withId(R.id.butShowSettings)).perform(click()); - - clickPreference(R.string.authentication_label); - onView(withId(R.id.usernameEditText)).perform(typeText(username)); - onView(withId(R.id.passwordEditText)).perform(typeText(password)); - onView(withText(R.string.confirm_label)).perform(click()); - - onView(isRoot()).perform(pressBack()); - } - } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 909b7a5a2..5940d511b 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -3,34 +3,28 @@ package de.test.antennapod.ui; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; - import androidx.annotation.StringRes; import androidx.preference.PreferenceManager; -import androidx.test.espresso.matcher.RootMatchers; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; - -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; -import org.awaitility.Awaitility; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.util.Arrays; -import java.util.concurrent.TimeUnit; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.test.antennapod.EspressoTestUtils; +import org.awaitility.Awaitility; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Arrays; -import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; @@ -49,7 +43,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.waitForView; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.hamcrest.Matchers.anything; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertTrue; @@ -78,22 +71,6 @@ public class PreferencesTest { } @Test - public void testSwitchTheme() { - final UserPreferences.ThemePreference theme = UserPreferences.getTheme(); - int otherThemeText; - if (theme == UserPreferences.ThemePreference.DARK) { - otherThemeText = R.string.pref_theme_title_light; - } else { - otherThemeText = R.string.pref_theme_title_dark; - } - clickPreference(R.string.user_interface_label); - clickPreference(R.string.pref_set_theme_title); - onView(withText(otherThemeText)).perform(click()); - Awaitility.await().atMost(1000, MILLISECONDS) - .until(() -> UserPreferences.getTheme() != theme); - } - - @Test public void testEnablePersistentPlaybackControls() { final boolean persistNotify = UserPreferences.isPersistNotify(); clickPreference(R.string.user_interface_label); @@ -146,6 +123,7 @@ public class PreferencesTest { doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING); doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT); doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK); + doTestEnqueueLocation(R.string.enqueue_location_random, EnqueueLocation.RANDOM); } private void doTestEnqueueLocation(@StringRes int optionResId, EnqueueLocation expected) { @@ -215,7 +193,7 @@ public class PreferencesTest { @Test public void testAutoDelete() { - clickPreference(R.string.storage_pref); + clickPreference(R.string.downloads_pref); final boolean autoDelete = UserPreferences.isAutoDelete(); onView(withText(R.string.pref_auto_delete_title)).perform(click()); Awaitility.await().atMost(1000, MILLISECONDS) @@ -246,32 +224,8 @@ public class PreferencesTest { } @Test - public void testDisableUpdateInterval() { - clickPreference(R.string.network_pref); - clickPreference(R.string.feed_refresh_title); - onView(withText(R.string.feed_refresh_never)).perform(click()); - onView(withId(R.id.disableRadioButton)).perform(click()); - onView(withText(R.string.confirm_label)).perform(click()); - Awaitility.await().atMost(1000, MILLISECONDS) - .until(() -> UserPreferences.getUpdateInterval() == 0); - } - - @Test - public void testSetUpdateInterval() { - clickPreference(R.string.network_pref); - clickPreference(R.string.feed_refresh_title); - onView(withId(R.id.intervalRadioButton)).perform(click()); - onView(withId(R.id.spinner)).perform(click()); - int position = 1; // an arbitrary position - onData(anything()).inRoot(RootMatchers.isPlatformPopup()).atPosition(position).perform(click()); - onView(withText(R.string.confirm_label)).perform(click()); - Awaitility.await().atMost(1000, MILLISECONDS) - .until(() -> UserPreferences.getUpdateInterval() == TimeUnit.HOURS.toMillis(2)); - } - - @Test public void testSetSequentialDownload() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_parallel_downloads_title); onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); onView(withClassName(endsWith("EditText"))).perform(replaceText("1")); @@ -282,7 +236,7 @@ public class PreferencesTest { @Test public void testSetParallelDownloads() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_parallel_downloads_title); onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); onView(withClassName(endsWith("EditText"))).perform(replaceText("10")); @@ -294,7 +248,7 @@ public class PreferencesTest { @Test public void testSetParallelDownloadsInvalidInput() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_parallel_downloads_title); onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); onView(withClassName(endsWith("EditText"))).perform(replaceText("0")); @@ -309,7 +263,7 @@ public class PreferencesTest { String[] values = res.getStringArray(R.array.episode_cache_size_values); String entry = entries[entries.length / 2]; final int value = Integer.parseInt(values[values.length / 2]); - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_automatic_download_title); clickPreference(R.string.pref_episode_cache_title); onView(isRoot()).perform(waitForView(withText(entry), 1000)); @@ -325,7 +279,7 @@ public class PreferencesTest { String minEntry = entries[0]; final int minValue = Integer.parseInt(values[0]); - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_automatic_download_title); clickPreference(R.string.pref_episode_cache_title); onView(withId(R.id.select_dialog_listview)).perform(swipeDown()); @@ -340,7 +294,7 @@ public class PreferencesTest { String[] values = res.getStringArray(R.array.episode_cache_size_values); String maxEntry = entries[entries.length - 1]; final int maxValue = Integer.parseInt(values[values.length - 1]); - onView(withText(R.string.network_pref)).perform(click()); + onView(withText(R.string.downloads_pref)).perform(click()); onView(withText(R.string.pref_automatic_download_title)).perform(click()); onView(withText(R.string.pref_episode_cache_title)).perform(click()); onView(withId(R.id.select_dialog_listview)).perform(swipeUp()); @@ -352,7 +306,7 @@ public class PreferencesTest { @Test public void testAutomaticDownload() { final boolean automaticDownload = UserPreferences.isEnableAutodownload(); - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_automatic_download_title); clickPreference(R.string.pref_automatic_download_title); Awaitility.await().atMost(1000, MILLISECONDS) @@ -373,7 +327,7 @@ public class PreferencesTest { @Test public void testEpisodeCleanupFavoriteOnly() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); onView(withId(R.id.select_dialog_listview)).perform(swipeDown()); @@ -384,7 +338,7 @@ public class PreferencesTest { @Test public void testEpisodeCleanupQueueOnly() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); onView(withId(R.id.select_dialog_listview)).perform(swipeDown()); @@ -395,7 +349,7 @@ public class PreferencesTest { @Test public void testEpisodeCleanupNeverAlg() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); onView(withId(R.id.select_dialog_listview)).perform(swipeUp()); @@ -406,7 +360,7 @@ public class PreferencesTest { @Test public void testEpisodeCleanupClassic() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); onView(withText(R.string.episode_cleanup_after_listening)).perform(click()); @@ -423,7 +377,7 @@ public class PreferencesTest { @Test public void testEpisodeCleanupNumDays() { - clickPreference(R.string.network_pref); + clickPreference(R.string.downloads_pref); clickPreference(R.string.pref_automatic_download_title); clickPreference(R.string.pref_episode_cleanup_title); String search = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, 3, 3); @@ -483,7 +437,7 @@ public class PreferencesTest { @Test public void testDeleteRemovesFromQueue() { - clickPreference(R.string.storage_pref); + clickPreference(R.string.downloads_pref); if (!UserPreferences.shouldDeleteRemoveFromQueue()) { clickPreference(R.string.pref_delete_removes_from_queue_title); Awaitility.await().atMost(1000, MILLISECONDS) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dfe8cd77b..8c779db26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> - <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <supports-screens android:anyDensity="true" @@ -77,7 +77,6 @@ <activity android:name=".activity.SplashActivity" - android:label="@string/app_name" android:configChanges="keyboardHidden|orientation|screenSize" android:exported="true"> <intent-filter> @@ -100,7 +99,6 @@ android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation" android:windowSoftInputMode="stateAlwaysHidden" android:launchMode="singleTask" - android:label="@string/app_name" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> @@ -112,14 +110,21 @@ android:host="antennapod.org" android:pathPrefix="/deeplink/main" android:scheme="https" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:host="antennapod.org" android:pathPrefix="/deeplink/search" android:scheme="https" /> </intent-filter> <intent-filter> - <action android:name="de.danoeh.antennapod.intents.MAIN_ACTIVITY" /> <category android:name="android.intent.category.DEFAULT" /> + <action android:name="de.danoeh.antennapod.intents.MAIN_ACTIVITY" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/> </intent-filter> </activity> @@ -127,7 +132,13 @@ <activity android:name=".activity.DownloadAuthenticationActivity" android:theme="@style/Theme.AntennaPod.Dark.Translucent" - android:launchMode="singleInstance"/> + android:exported="false" + android:launchMode="singleInstance"> + <intent-filter> + <action android:name="de.danoeh.antennapod.intents.DOWNLOAD_AUTH_ACTIVITY" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> <activity android:name=".activity.PreferenceActivity" @@ -377,6 +388,7 @@ </provider> <meta-data + tools:ignore="Deprecated" android:name="com.google.android.actions" android:resource="@xml/actions" /> </application> diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java index 7cbe32e11..942e2cf24 100644 --- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -12,7 +12,6 @@ import com.joanzapata.iconify.fonts.MaterialModule; import de.danoeh.antennapod.activity.SplashActivity; import de.danoeh.antennapod.config.ApplicationCallbacksImpl; -import de.danoeh.antennapod.config.DownloadServiceCallbacksImpl; import de.danoeh.antennapod.core.ApCoreEventBusIndex; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.ClientConfigurator; @@ -35,7 +34,6 @@ public class PodcastApp extends MultiDexApplication { super.onCreate(); ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME; ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); - ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); Thread.setDefaultUncaughtExceptionHandler(new CrashReportWriter()); RxJavaErrorHandlerSetup.setupRxJavaErrorHandler(); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java index 82b9d3d25..176c3c990 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -12,10 +12,10 @@ import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.dialog.AuthenticationDialog; +import de.danoeh.antennapod.ui.appstartintent.DownloadAuthenticationActivityStarter; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.Validate; /** @@ -23,19 +23,13 @@ import org.apache.commons.lang3.Validate; * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value. */ public class DownloadAuthenticationActivity extends AppCompatActivity { - - /** - * The download request object that contains information about the resource that requires a username and a password. - */ - public static final String ARG_DOWNLOAD_REQUEST = "request"; - @Override protected void onCreate(Bundle savedInstanceState) { setTheme(ThemeSwitcher.getTranslucentTheme(this)); super.onCreate(savedInstanceState); - Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing"); - DownloadRequest request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST); + DownloadRequest request = getIntent().getParcelableExtra( + DownloadAuthenticationActivityStarter.EXTRA_DOWNLOAD_REQUEST); new AuthenticationDialog(this, R.string.authentication_label, true, "", "") { @Override 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 6782e9274..bd467076a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -22,36 +22,31 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; -import com.google.android.material.appbar.MaterialToolbar; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.recyclerview.widget.RecyclerView; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; import com.bumptech.glide.Glide; +import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.core.preferences.ThemeSwitcher; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.Validate; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.core.preferences.ThemeSwitcher; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.fragment.AddFeedFragment; +import de.danoeh.antennapod.fragment.AllEpisodesFragment; import de.danoeh.antennapod.fragment.AudioPlayerFragment; import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.InboxFragment; import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.fragment.InboxFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; @@ -60,10 +55,16 @@ import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.playback.cast.CastEnabledActivity; import de.danoeh.antennapod.preferences.PreferenceUpgrader; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.ui.home.HomeFragment; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * The activity that is shown when the user launches the app. @@ -76,8 +77,6 @@ public class MainActivity extends CastEnabledActivity { public static final String PREF_NAME = "MainActivityPrefs"; public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch"; - 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_REFRESH_ON_START = "refresh_on_start"; public static final String EXTRA_STARTED_FROM_SEARCH = "started_from_search"; @@ -159,6 +158,21 @@ public class MainActivity extends CastEnabledActivity { sheetBehavior = (LockableBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet); sheetBehavior.setHideable(false); sheetBehavior.setBottomSheetCallback(bottomSheetCallback); + + FeedUpdateManager.restartUpdateAlarm(this, false); + WorkManager.getInstance(this) + .getWorkInfosByTagLiveData(FeedUpdateManager.WORK_TAG_FEED_UPDATE) + .observe(this, workInfos -> { + boolean isRefreshingFeeds = false; + for (WorkInfo workInfo : workInfos) { + if (workInfo.getState() == WorkInfo.State.RUNNING) { + isRefreshingFeeds = true; + } else if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + isRefreshingFeeds = true; + } + } + EventBus.getDefault().postSticky(new FeedUpdateRunningEvent(isRefreshingFeeds)); + }); } @Override @@ -232,12 +246,18 @@ public class MainActivity extends CastEnabledActivity { } } + @Override + protected void onDestroy() { + super.onDestroy(); + if (drawerLayout != null) { + drawerLayout.removeDrawerListener(drawerToggle); + } + } + private void checkFirstLaunch() { SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { - // for backward compatibility, we only change defaults for fresh installs - UserPreferences.setUpdateInterval(12); - AutoUpdateManager.restartUpdateAlarm(this); + FeedUpdateManager.restartUpdateAlarm(this, true); SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(PREF_IS_FIRST_LAUNCH, false); @@ -441,6 +461,9 @@ public class MainActivity extends CastEnabledActivity { finish(); startActivity(new Intent(this, MainActivity.class)); } + if (UserPreferences.getHiddenDrawerItems().contains(NavDrawerFragment.getLastNavFragment(this))) { + loadFragment(UserPreferences.getDefaultPage(), null); + } } @Override @@ -515,20 +538,12 @@ public class MainActivity extends CastEnabledActivity { } private void handleNavIntent() { + Log.d(TAG, "handleNavIntent()"); Intent intent = getIntent(); - if (intent.hasExtra(EXTRA_FEED_ID) || intent.hasExtra(EXTRA_FRAGMENT_TAG) || intent.hasExtra(EXTRA_REFRESH_ON_START)) { - Log.d(TAG, "handleNavIntent()"); - String tag = intent.getStringExtra(EXTRA_FRAGMENT_TAG); - Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS); - boolean refreshOnStart = intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false); - if (refreshOnStart) { - AutoUpdateManager.runImmediate(this); - } - + if (intent.hasExtra(EXTRA_FEED_ID)) { long feedId = intent.getLongExtra(EXTRA_FEED_ID, 0); - if (tag != null) { - loadFragment(tag, args); - } else if (feedId > 0) { + Bundle args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS); + if (feedId > 0) { boolean startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false); boolean addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false); if (startedFromSearch || addToBackStack) { @@ -538,12 +553,26 @@ public class MainActivity extends CastEnabledActivity { } } sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } else if (intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)) { + String tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG); + Bundle args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS); + if (tag != null) { + loadFragment(tag, args); + } + sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) { sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); bottomSheetCallback.onSlide(null, 1.0f); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { handleDeeplink(intent.getData()); } + + if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false) && drawerLayout != null) { + drawerLayout.open(); + } + if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) { + FeedUpdateManager.runOnceOrAsk(this); + } // to avoid handling the intent twice when the configuration changes setIntent(new Intent(MainActivity.this, MainActivity.class)); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 50aabbd01..f5f3d28f6 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -36,7 +36,7 @@ import de.danoeh.antennapod.core.preferences.ThemeSwitcher; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; import de.danoeh.antennapod.core.util.DownloadErrorLabel; import de.danoeh.antennapod.event.FeedListUpdateEvent; @@ -455,10 +455,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { if (feedInFeedlist()) { openFeed(); } else { - Feed f = new Feed(selectedDownloadUrl, null, feed.getTitle()); - DownloadServiceInterface.get().download(this, false, DownloadRequestCreator.create(f) - .withAuthentication(username, password) - .build()); + DBTasks.updateFeed(this, feed, false); didPressSubscribe = true; handleUpdatedFeedStatus(); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java index c8d9f9b31..10a41057c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java @@ -28,8 +28,8 @@ import de.danoeh.antennapod.core.export.opml.OpmlElement; import de.danoeh.antennapod.core.export.opml.OpmlReader; import de.danoeh.antennapod.core.preferences.ThemeSwitcher; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.databinding.OpmlSelectionBinding; import de.danoeh.antennapod.model.feed.Feed; import io.reactivex.Completable; @@ -43,6 +43,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -95,9 +96,12 @@ public class OpmlImportActivity extends AppCompatActivity { continue; } OpmlElement element = readElements.get(checked.keyAt(i)); - Feed feed = new Feed(element.getXmlUrl(), null, element.getText()); - DownloadServiceInterface.get().download(this, false, DownloadRequestCreator.create(feed).build()); + Feed feed = new Feed(element.getXmlUrl(), null, + element.getText() != null ? element.getText() : "Unknown podcast"); + feed.setItems(Collections.emptyList()); + DBTasks.updateFeed(this, feed, false); } + FeedUpdateManager.runOnce(this); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -109,6 +113,7 @@ public class OpmlImportActivity extends AppCompatActivity { startActivity(intent); finish(); }, e -> { + e.printStackTrace(); viewBinding.progressBar.setVisibility(View.GONE); Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); }); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index af8db0d79..b101c20c0 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -23,10 +23,9 @@ import de.danoeh.antennapod.databinding.SettingsActivityBinding; import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.DownloadsPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment; import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment; import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment; @@ -69,10 +68,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe if (screen == R.xml.preferences_user_interface) { prefFragment = new UserInterfacePreferencesFragment(); - } else if (screen == R.xml.preferences_network) { - prefFragment = new NetworkPreferencesFragment(); - } else if (screen == R.xml.preferences_storage) { - prefFragment = new StoragePreferencesFragment(); + } else if (screen == R.xml.preferences_downloads) { + prefFragment = new DownloadsPreferencesFragment(); } else if (screen == R.xml.preferences_import_export) { prefFragment = new ImportExportPreferencesFragment(); } else if (screen == R.xml.preferences_autodownload) { @@ -90,14 +87,12 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe } public static int getTitleOfPage(int preferences) { - if (preferences == R.xml.preferences_network) { - return R.string.network_pref; + if (preferences == R.xml.preferences_downloads) { + return R.string.downloads_pref; } else if (preferences == R.xml.preferences_autodownload) { return R.string.pref_automatic_download_title; } else if (preferences == R.xml.preferences_playback) { return R.string.playback_pref; - } else if (preferences == R.xml.preferences_storage) { - return R.string.storage_pref; } else if (preferences == R.xml.preferences_import_export) { return R.string.import_export_pref; } else if (preferences == R.xml.preferences_user_interface) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java index 48f91672a..34e93bf22 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java @@ -31,6 +31,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.databinding.SubscriptionSelectionActivityBinding; import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -142,7 +143,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity { } disposable = Observable.fromCallable( () -> { - NavDrawerData data = DBReader.getNavDrawerData(); + NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()); return getFeedItems(data.items, new ArrayList<>()); }) .subscribeOn(Schedulers.io()) 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 034fd71fc..9070cd6f8 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -30,17 +30,18 @@ import android.widget.EditText; import android.widget.FrameLayout; import android.widget.SeekBar; import androidx.annotation.Nullable; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.core.view.WindowCompat; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import com.bumptech.glide.Glide; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.fragment.ChaptersFragment; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; @@ -514,11 +515,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar. @Subscribe(threadMode = ThreadMode.MAIN) public void onMediaPlayerError(PlayerErrorEvent event) { - final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(VideoplayerActivity.this); - errorDialog.setTitle(R.string.error_label); - errorDialog.setMessage(event.getMessage()); - errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish()); - errorDialog.show(); + MediaPlayerErrorDialog.show(this, event); } @Override @@ -561,6 +558,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar. menu.findItem(R.id.player_switch_to_audio_only).setVisible(true); menu.findItem(R.id.audio_controls).setIcon(R.drawable.ic_sliders); menu.findItem(R.id.playback_speed).setVisible(true); + menu.findItem(R.id.player_show_chapters).setVisible(true); return true; } @@ -570,13 +568,15 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar. switchToAudioOnly = true; finish(); return true; - } - if (item.getItemId() == android.R.id.home) { + } else if (item.getItemId() == android.R.id.home) { Intent intent = new Intent(VideoplayerActivity.this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); finish(); return true; + } else if (item.getItemId() == R.id.player_show_chapters) { + new ChaptersFragment().show(getSupportFragmentManager(), ChaptersFragment.TAG); + return true; } if (controller == null) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java index fdbcde54b..c33af33a4 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java @@ -3,6 +3,8 @@ package de.danoeh.antennapod.activity; import android.appwidget.AppWidgetManager; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.CheckBox; @@ -84,6 +86,23 @@ public class WidgetConfigActivity extends AppCompatActivity { ckFastForward.setOnClickListener(v -> displayPreviewPanel()); ckSkip = findViewById(R.id.ckSkip); ckSkip.setOnClickListener(v -> displayPreviewPanel()); + + setInitialState(); + } + + private void setInitialState() { + SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE); + ckPlaybackSpeed.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false)); + ckRewind.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, false)); + ckFastForward.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, false)); + ckSkip.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, false)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + int color = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, 0); + int opacity = Color.alpha(color) * 100 / 0xFF; + + opacitySeekBar.setProgress(opacity, false); + } + displayPreviewPanel(); } private void displayPreviewPanel() { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java index dc82e3adc..1d06bf07c 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java @@ -1,40 +1,28 @@ package de.danoeh.antennapod.adapter; -import android.content.Context; import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; -import androidx.palette.graphics.Palette; - import android.view.View; import android.widget.ImageView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomViewTarget; - -import java.lang.ref.WeakReference; - import com.bumptech.glide.request.transition.Transition; - -import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.glide.PaletteBitmap; + +import java.lang.ref.WeakReference; public class CoverLoader { private int resource = 0; private String uri; private String fallbackUri; - private TextView txtvPlaceholder; private ImageView imgvCover; private boolean textAndImageCombined; private MainActivity activity; + private TextView fallbackTitle; public CoverLoader(MainActivity activity) { this.activity = activity; @@ -60,31 +48,30 @@ public class CoverLoader { return this; } - public CoverLoader withPlaceholderView(TextView placeholderView) { - txtvPlaceholder = placeholderView; + public CoverLoader withPlaceholderView(TextView title) { + this.fallbackTitle = title; return this; } /** * Set cover text and if it should be shown even if there is a cover image. - * - * @param placeholderView Cover text. + * @param fallbackTitle Fallback title text * @param textAndImageCombined Show cover text even if there is a cover image? */ @NonNull - public CoverLoader withPlaceholderView(@NonNull TextView placeholderView, boolean textAndImageCombined) { - this.txtvPlaceholder = placeholderView; + public CoverLoader withPlaceholderView(TextView fallbackTitle, boolean textAndImageCombined) { + this.fallbackTitle = fallbackTitle; this.textAndImageCombined = textAndImageCombined; return this; } public void load() { - CoverTarget coverTarget = new CoverTarget(txtvPlaceholder, imgvCover, textAndImageCombined); + CoverTarget coverTarget = new CoverTarget(fallbackTitle, imgvCover, textAndImageCombined); if (resource != 0) { Glide.with(activity).clear(coverTarget); imgvCover.setImageResource(resource); - CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined, null); + CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined); return; } @@ -92,14 +79,14 @@ public class CoverLoader { .fitCenter() .dontAnimate(); - RequestBuilder<PaletteBitmap> builder = Glide.with(activity) - .as(PaletteBitmap.class) + RequestBuilder<Drawable> builder = Glide.with(activity) + .as(Drawable.class) .load(uri) .apply(options); - if (fallbackUri != null && txtvPlaceholder != null && imgvCover != null) { + if (fallbackUri != null) { builder = builder.error(Glide.with(activity) - .as(PaletteBitmap.class) + .as(Drawable.class) .load(fallbackUri) .apply(options)); } @@ -107,64 +94,41 @@ public class CoverLoader { builder.into(coverTarget); } - static class CoverTarget extends CustomViewTarget<ImageView, PaletteBitmap> { - private final WeakReference<TextView> placeholder; + static class CoverTarget extends CustomViewTarget<ImageView, Drawable> { + private final WeakReference<TextView> fallbackTitle; private final WeakReference<ImageView> cover; - private boolean textAndImageCombined; + private final boolean textAndImageCombined; - public CoverTarget(TextView txtvPlaceholder, ImageView imgvCover, boolean textAndImageCombined) { - super(imgvCover); - if (txtvPlaceholder != null) { - txtvPlaceholder.setVisibility(View.VISIBLE); - } - placeholder = new WeakReference<>(txtvPlaceholder); - cover = new WeakReference<>(imgvCover); + public CoverTarget(TextView fallbackTitle, ImageView coverImage, boolean textAndImageCombined) { + super(coverImage); + this.fallbackTitle = new WeakReference<>(fallbackTitle); + this.cover = new WeakReference<>(coverImage); this.textAndImageCombined = textAndImageCombined; } @Override public void onLoadFailed(Drawable errorDrawable) { - setPlaceholderVisibility(this.placeholder.get(), true, null); + setTitleVisibility(fallbackTitle.get(), true); } @Override - public void onResourceReady(@NonNull PaletteBitmap resource, - @Nullable Transition<? super PaletteBitmap> transition) { + public void onResourceReady(@NonNull Drawable resource, + @Nullable Transition<? super Drawable> transition) { ImageView ivCover = cover.get(); - ivCover.setImageBitmap(resource.bitmap); - setPlaceholderVisibility(placeholder.get(), textAndImageCombined, resource.palette); + ivCover.setImageDrawable(resource); + setTitleVisibility(fallbackTitle.get(), textAndImageCombined); } @Override protected void onResourceCleared(@Nullable Drawable placeholder) { ImageView ivCover = cover.get(); ivCover.setImageDrawable(placeholder); - setPlaceholderVisibility(this.placeholder.get(), textAndImageCombined, null); + setTitleVisibility(fallbackTitle.get(), textAndImageCombined); } - static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined, Palette palette) { - boolean showTitle = UserPreferences.shouldShowSubscriptionTitle(); - if (placeholder != null) { - if (textAndImageCombined || showTitle) { - final Context context = placeholder.getContext(); - placeholder.setVisibility(View.VISIBLE); - int bgColor = ContextCompat.getColor(context, R.color.feed_text_bg); - if (palette == null || !showTitle) { - placeholder.setBackgroundColor(bgColor); - placeholder.setTextColor(ThemeUtils.getColorFromAttr(placeholder.getContext(), - android.R.attr.textColorPrimary)); - return; - } - int dominantColor = palette.getDominantColor(bgColor); - int textColor = ContextCompat.getColor(context, R.color.white); - if (ColorUtils.calculateLuminance(dominantColor) > 0.5) { - textColor = ContextCompat.getColor(context, R.color.black); - } - placeholder.setTextColor(textColor); - placeholder.setBackgroundColor(dominantColor); - } else { - placeholder.setVisibility(View.INVISIBLE); - } + static void setTitleVisibility(TextView fallbackTitle, boolean textAndImageCombined) { + if (fallbackTitle != null) { + fallbackTitle.setVisibility(textAndImageCombined ? View.VISIBLE : View.GONE); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java index 9d2713f48..9a578da6a 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java @@ -16,10 +16,17 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFeedListAdapter.Holder> { +import android.view.ContextMenu; +import android.view.MenuInflater; +import androidx.annotation.Nullable; + +public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFeedListAdapter.Holder> + implements View.OnCreateContextMenuListener { private final WeakReference<MainActivity> mainActivityRef; private final List<Feed> data = new ArrayList<>(); private int dummyViews = 0; + private Feed longPressedItem; + public HorizontalFeedListAdapter(MainActivity mainActivity) { this.mainActivityRef = new WeakReference<>(mainActivity); @@ -57,6 +64,13 @@ public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFe holder.imageView.setOnClickListener(v -> mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId()))); + holder.imageView.setOnCreateContextMenuListener(this); + holder.imageView.setOnLongClickListener(v -> { + int currentItemPosition = holder.getBindingAdapterPosition(); + longPressedItem = data.get(currentItemPosition); + return false; + }); + Glide.with(mainActivityRef.get()) .load(podcast.getImageUrl()) .apply(new RequestOptions() @@ -66,6 +80,11 @@ public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFe .into(holder.imageView); } + @Nullable + public Feed getLongPressedItem() { + return longPressedItem; + } + @Override public long getItemId(int position) { if (position >= data.size()) { @@ -79,6 +98,16 @@ public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFe return dummyViews + data.size(); } + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (longPressedItem == null) { + return; + } + inflater.inflate(R.menu.nav_feed_context, contextMenu); + contextMenu.setHeaderTitle(longPressedItem.getTitle()); + } + static class Holder extends RecyclerView.ViewHolder { SquareImageView imageView; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java index 7be417984..bfa37b600 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java @@ -5,7 +5,6 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; -import android.text.TextUtils; import android.view.ContextMenu; import android.view.InputDevice; import android.view.LayoutInflater; @@ -17,20 +16,19 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; +import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.elevation.SurfaceColors; import java.lang.ref.WeakReference; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; @@ -39,7 +37,6 @@ import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.ui.common.TriangleLabelView; /** * Adapter for subscriptions @@ -52,6 +49,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription private List<NavDrawerData.DrawerItem> listItems; private NavDrawerData.DrawerItem selectedItem = null; int longPressedPosition = 0; // used to init actionMode + private int columnCount = 3; public SubscriptionsRecyclerAdapter(MainActivity mainActivity) { super(mainActivity); @@ -60,6 +58,10 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription setHasStableIds(true); } + public void setColumnCount(int columnCount) { + this.columnCount = columnCount; + } + public Object getItem(int position) { return listItems.get(position); } @@ -72,24 +74,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription @Override public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false); - TextView feedTitle = itemView.findViewById(R.id.txtvTitle); - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) feedTitle.getLayoutParams(); - int topAndBottomItemId = R.id.imgvCover; - int belowItemId = 0; - - if (viewType == COVER_WITH_TITLE) { - topAndBottomItemId = 0; - belowItemId = R.id.imgvCover; - feedTitle.setBackgroundColor( - ContextCompat.getColor(feedTitle.getContext(), R.color.feed_text_bg)); - int padding = (int) convertDpToPixel(feedTitle.getContext(), 6); - feedTitle.setPadding(padding, padding, padding, padding); - } - params.addRule(RelativeLayout.BELOW, belowItemId); - params.addRule(RelativeLayout.ALIGN_TOP, topAndBottomItemId); - params.addRule(RelativeLayout.ALIGN_BOTTOM, topAndBottomItemId); - feedTitle.setLayoutParams(params); - feedTitle.setSingleLine(viewType == COVER_WITH_TITLE); + itemView.findViewById(R.id.titleLabel).setVisibility(viewType == COVER_WITH_TITLE ? View.VISIBLE : View.GONE); return new SubscriptionViewHolder(itemView); } @@ -107,11 +92,11 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription holder.selectCheckbox.setChecked((isSelected(position))); holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> setSelected(holder.getBindingAdapterPosition(), isChecked)); - holder.imageView.setAlpha(0.6f); + holder.coverImage.setAlpha(0.6f); holder.count.setVisibility(View.GONE); } else { holder.selectView.setVisibility(View.GONE); - holder.imageView.setAlpha(1.0f); + holder.coverImage.setAlpha(1.0f); } holder.itemView.setOnLongClickListener(v -> { @@ -224,55 +209,74 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription } public class SubscriptionViewHolder extends RecyclerView.ViewHolder { - private final TextView feedTitle; - private final ImageView imageView; - private final TriangleLabelView count; + private final TextView title; + private final ImageView coverImage; + private final TextView count; + private final TextView fallbackTitle; private final FrameLayout selectView; private final CheckBox selectCheckbox; + private final CardView card; public SubscriptionViewHolder(@NonNull View itemView) { super(itemView); - feedTitle = itemView.findViewById(R.id.txtvTitle); - imageView = itemView.findViewById(R.id.imgvCover); - count = itemView.findViewById(R.id.triangleCountView); - selectView = itemView.findViewById(R.id.selectView); + title = itemView.findViewById(R.id.titleLabel); + coverImage = itemView.findViewById(R.id.coverImage); + count = itemView.findViewById(R.id.countViewPill); + fallbackTitle = itemView.findViewById(R.id.fallbackTitleLabel); + selectView = itemView.findViewById(R.id.selectContainer); selectCheckbox = itemView.findViewById(R.id.selectCheckBox); + card = itemView.findViewById(R.id.outerContainer); } public void bind(NavDrawerData.DrawerItem drawerItem) { Drawable drawable = AppCompatResources.getDrawable(selectView.getContext(), R.drawable.ic_checkbox_background); selectView.setBackground(drawable); // Setting this in XML crashes API <= 21 - feedTitle.setText(drawerItem.getTitle()); - imageView.setContentDescription(drawerItem.getTitle()); - feedTitle.setVisibility(View.VISIBLE); - if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL) { - count.setCorner(TriangleLabelView.Corner.TOP_LEFT); - } - + title.setText(drawerItem.getTitle()); + fallbackTitle.setText(drawerItem.getTitle()); + coverImage.setContentDescription(drawerItem.getTitle()); if (drawerItem.getCounter() > 0) { - count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter())); + count.setText(NumberFormat.getInstance().format(drawerItem.getCounter())); count.setVisibility(View.VISIBLE); } else { count.setVisibility(View.GONE); } + CoverLoader coverLoader = new CoverLoader(mainActivityRef.get()); + boolean textAndImageCombined; if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; - boolean textAndImageCombind = feed.isLocalFeed() - && feed.getImageUrl() != null && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER); - new CoverLoader(mainActivityRef.get()) - .withUri(feed.getImageUrl()) - .withPlaceholderView(feedTitle, textAndImageCombind) - .withCoverView(imageView) - .load(); + textAndImageCombined = feed.isLocalFeed() && feed.getImageUrl() != null + && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER); + coverLoader.withUri(feed.getImageUrl()); } else { - new CoverLoader(mainActivityRef.get()) - .withResource(R.drawable.ic_tag) - .withPlaceholderView(feedTitle, true) - .withCoverView(imageView) - .load(); + textAndImageCombined = true; + coverLoader.withResource(R.drawable.ic_tag); + } + if (UserPreferences.shouldShowSubscriptionTitle()) { + // No need for fallback title when already showing title + fallbackTitle.setVisibility(View.GONE); + } else { + coverLoader.withPlaceholderView(fallbackTitle, textAndImageCombined); + } + coverLoader.withCoverView(coverImage); + coverLoader.load(); + + float density = mainActivityRef.get().getResources().getDisplayMetrics().density; + card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActivityRef.get(), 1 * density)); + + int textPadding = columnCount <= 3 ? 16 : 8; + title.setPadding(textPadding, textPadding, textPadding, textPadding); + fallbackTitle.setPadding(textPadding, textPadding, textPadding, textPadding); + + int textSize = 14; + if (columnCount == 3) { + textSize = 15; + } else if (columnCount == 2) { + textSize = 16; } + title.setTextSize(textSize); + fallbackTitle.setTextSize(textSize); } } diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java deleted file mode 100644 index 69f112c3b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.config; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; - -import android.os.Bundle; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.DownloadServiceCallbacks; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.QueueFragment; - - -public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { - - @Override - public PendingIntent getNotificationContentIntent(Context context) { - Intent intent = new Intent(context, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, CompletedDownloadsFragment.TAG); - return PendingIntent.getActivity(context, - R.id.pending_intent_download_service_notification, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - @Override - public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) { - final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class); - activityIntent.setAction("request" + request.getFeedfileId()); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request); - return PendingIntent.getActivity(context.getApplicationContext(), - request.getSource().hashCode(), activityIntent, - PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - @Override - public PendingIntent getReportNotificationContentIntent(Context context) { - Intent intent = new Intent(context, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, CompletedDownloadsFragment.TAG); - Bundle args = new Bundle(); - args.putBoolean(CompletedDownloadsFragment.ARG_SHOW_LOGS, true); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - @Override - public PendingIntent getAutoDownloadReportNotificationContentIntent(Context context) { - Intent intent = new Intent(context, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, QueueFragment.TAG); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java deleted file mode 100644 index 3d92fd979..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java +++ /dev/null @@ -1,117 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.content.res.Resources; -import android.os.Build; -import android.text.format.DateFormat; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ArrayAdapter; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; -import de.danoeh.antennapod.databinding.FeedRefreshDialogBinding; -import de.danoeh.antennapod.databinding.ScrollableDialogBinding; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import java.util.concurrent.TimeUnit; -import org.apache.commons.lang3.ArrayUtils; - -public class FeedRefreshIntervalDialog { - private static final int[] INTERVAL_VALUES_HOURS = {1, 2, 4, 8, 12, 24, 72}; - private final Context context; - private FeedRefreshDialogBinding viewBinding; - - public FeedRefreshIntervalDialog(Context context) { - this.context = context; - } - - public void show() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(R.string.feed_refresh_title); - builder.setMessage(R.string.feed_refresh_sum); - ScrollableDialogBinding scrollableDialogBinding = ScrollableDialogBinding.inflate(LayoutInflater.from(context)); - builder.setView(scrollableDialogBinding.getRoot()); - viewBinding = FeedRefreshDialogBinding.inflate(LayoutInflater.from(context)); - scrollableDialogBinding.content.addView(viewBinding.getRoot()); - - ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(context, - android.R.layout.simple_spinner_item, buildSpinnerEntries()); - spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - viewBinding.spinner.setAdapter(spinnerArrayAdapter); - viewBinding.timePicker.setIs24HourView(DateFormat.is24HourFormat(context)); - viewBinding.spinner.setSelection(ArrayUtils.indexOf(INTERVAL_VALUES_HOURS, 24)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - viewBinding.timePicker.setHour(8); - viewBinding.timePicker.setMinute(0); - } else { - viewBinding.timePicker.setCurrentHour(8); - viewBinding.timePicker.setCurrentMinute(0); - } - - long currInterval = UserPreferences.getUpdateInterval(); - int[] updateTime = UserPreferences.getUpdateTimeOfDay(); - if (currInterval > 0) { - viewBinding.spinner.setSelection(ArrayUtils.indexOf(INTERVAL_VALUES_HOURS, - (int) TimeUnit.MILLISECONDS.toHours(currInterval))); - viewBinding.intervalRadioButton.setChecked(true); - } else if (updateTime.length == 2 && updateTime[0] >= 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - viewBinding.timePicker.setHour(updateTime[0]); - viewBinding.timePicker.setMinute(updateTime[1]); - } else { - viewBinding.timePicker.setCurrentHour(updateTime[0]); - viewBinding.timePicker.setCurrentMinute(updateTime[1]); - } - viewBinding.timeRadioButton.setChecked(true); - } else { - viewBinding.disableRadioButton.setChecked(true); - } - updateVisibility(); - - viewBinding.radioGroup.setOnCheckedChangeListener((radioGroup, i) -> updateVisibility()); - - AlertDialog dialog = builder.show(); - - scrollableDialogBinding.positiveButton.setText(R.string.confirm_label); - scrollableDialogBinding.positiveButton.setOnClickListener(v -> { - dialog.dismiss(); - if (viewBinding.intervalRadioButton.isChecked()) { - UserPreferences.setUpdateInterval(INTERVAL_VALUES_HOURS[viewBinding.spinner.getSelectedItemPosition()]); - AutoUpdateManager.restartUpdateAlarm(context); - } else if (viewBinding.timeRadioButton.isChecked()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getHour(), - viewBinding.timePicker.getMinute()); - } else { - UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getCurrentHour(), - viewBinding.timePicker.getCurrentMinute()); - } - AutoUpdateManager.restartUpdateAlarm(context); - } else if (viewBinding.disableRadioButton.isChecked()) { - UserPreferences.disableAutoUpdate(); - AutoUpdateManager.disableAutoUpdate(context); - } else { - throw new IllegalStateException("Unexpected error."); - } - }); - - scrollableDialogBinding.negativeButton.setText(R.string.cancel_label); - scrollableDialogBinding.negativeButton.setOnClickListener((v) -> dialog.dismiss()); - } - - private String[] buildSpinnerEntries() { - final Resources res = context.getResources(); - String[] entries = new String[INTERVAL_VALUES_HOURS.length]; - for (int i = 0; i < INTERVAL_VALUES_HOURS.length; i++) { - int hours = INTERVAL_VALUES_HOURS[i]; - entries[i] = res.getQuantityString(R.plurals.feed_refresh_every_x_hours, hours, hours); - } - return entries; - } - - private void updateVisibility() { - viewBinding.spinner.setVisibility(viewBinding.intervalRadioButton.isChecked() ? View.VISIBLE : View.GONE); - viewBinding.timePicker.setVisibility(viewBinding.timeRadioButton.isChecked() ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java new file mode 100644 index 000000000..306400b29 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Activity; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.event.PlayerErrorEvent; + +public class MediaPlayerErrorDialog { + public static void show(Activity activity, PlayerErrorEvent event) { + final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(activity); + errorDialog.setTitle(R.string.error_label); + + String genericMessage = activity.getString(R.string.playback_error_generic); + SpannableString errorMessage = new SpannableString(genericMessage + "\n\n" + event.getMessage()); + errorMessage.setSpan(new ForegroundColorSpan(0x88888888), + genericMessage.length(), errorMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + errorDialog.setMessage(errorMessage); + errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) -> + ((MainActivity) activity).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); + errorDialog.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java index cec41fac0..a87dccdf5 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -63,31 +63,12 @@ public class PlaybackControlsDialog extends DialogFragment { } private void setupUi() { - final CheckBox stereoToMono = dialog.findViewById(R.id.stereo_to_mono); - stereoToMono.setChecked(UserPreferences.stereoToMono()); - if (controller != null && !controller.canDownmix()) { - stereoToMono.setEnabled(false); - String sonicOnly = getString(R.string.sonic_only); - stereoToMono.setText(getString(R.string.stereo_to_mono) + " [" + sonicOnly + "]"); - } - final CheckBox skipSilence = dialog.findViewById(R.id.skipSilence); skipSilence.setChecked(UserPreferences.isSkipSilence()); - if (!UserPreferences.useExoplayer()) { - skipSilence.setEnabled(false); - String exoplayerOnly = getString(R.string.exoplayer_only); - skipSilence.setText(getString(R.string.pref_skip_silence_title) + " [" + exoplayerOnly + "]"); - } skipSilence.setOnCheckedChangeListener((buttonView, isChecked) -> { UserPreferences.setSkipSilence(isChecked); controller.setSkipSilence(isChecked); }); - stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> { - UserPreferences.stereoToMono(isChecked); - if (controller != null) { - controller.setDownmix(isChecked); - } - }); } private void setupAudioTracks() { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java index 23c032248..363b87ca6 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java @@ -5,6 +5,8 @@ import android.content.Context; import android.content.DialogInterface; import android.util.Log; +import androidx.annotation.Nullable; + import java.util.Collections; import java.util.List; @@ -19,21 +21,26 @@ import io.reactivex.schedulers.Schedulers; public class RemoveFeedDialog { private static final String TAG = "RemoveFeedDialog"; - public static void show(Context context, Feed feed) { + public static void show(Context context, Feed feed, @Nullable Runnable callback) { List<Feed> feeds = Collections.singletonList(feed); String message = getMessageId(context, feeds); - showDialog(context, feeds, message); + showDialog(context, feeds, message, callback); } public static void show(Context context, List<Feed> feeds) { String message = getMessageId(context, feeds); - showDialog(context, feeds, message); + showDialog(context, feeds, message, null); } - private static void showDialog(Context context, List<Feed> feeds, String message) { + private static void showDialog(Context context, List<Feed> feeds, String message, @Nullable Runnable callback) { ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) { @Override public void onConfirmButtonPressed(DialogInterface clickedDialog) { + + if (callback != null) { + callback.run(); + } + clickedDialog.dismiss(); ProgressDialog progressDialog = new ProgressDialog(context); diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index 52e6f7807..ecbc1d873 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.os.Bundle; +import android.text.format.DateFormat; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Button; @@ -11,19 +12,25 @@ import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; + import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.Locale; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; public class SleepTimerDialog extends DialogFragment { private PlaybackController controller; @@ -31,6 +38,7 @@ public class SleepTimerDialog extends DialogFragment { private LinearLayout timeSetup; private LinearLayout timeDisplay; private TextView time; + private CheckBox chAutoEnable; public SleepTimerDialog() { @@ -99,20 +107,38 @@ public class SleepTimerDialog extends DialogFragment { imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); }, 100); - CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset); - CheckBox cbVibrate = content.findViewById(R.id.cbVibrate); - CheckBox chAutoEnable = content.findViewById(R.id.chAutoEnable); + final CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset); + final CheckBox cbVibrate = content.findViewById(R.id.cbVibrate); + chAutoEnable = content.findViewById(R.id.chAutoEnable); + final TextView changeTimesButton = content.findViewById(R.id.changeTimes); cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset()); cbVibrate.setChecked(SleepTimerPreferences.vibrate()); chAutoEnable.setChecked(SleepTimerPreferences.autoEnable()); + changeTimesButton.setEnabled(chAutoEnable.isChecked()); cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked) -> SleepTimerPreferences.setShakeToReset(isChecked)); cbVibrate.setOnCheckedChangeListener((buttonView, isChecked) -> SleepTimerPreferences.setVibrate(isChecked)); chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked) - -> SleepTimerPreferences.setAutoEnable(isChecked)); + -> { + SleepTimerPreferences.setAutoEnable(isChecked); + changeTimesButton.setEnabled(isChecked); + }); + updateAutoEnableText(); + + changeTimesButton.setOnClickListener(changeTimesBtn -> { + int from = SleepTimerPreferences.autoEnableFrom(); + int to = SleepTimerPreferences.autoEnableTo(); + TimeRangeDialog dialog = new TimeRangeDialog(getContext(), from, to); + dialog.setOnDismissListener(v -> { + SleepTimerPreferences.setAutoEnableFrom(dialog.getFrom()); + SleepTimerPreferences.setAutoEnableTo(dialog.getTo()); + updateAutoEnableText(); + }); + dialog.show(); + }); Button disableButton = content.findViewById(R.id.disableSleeptimerButton); disableButton.setOnClickListener(v -> { @@ -144,6 +170,28 @@ public class SleepTimerDialog extends DialogFragment { return builder.create(); } + private void updateAutoEnableText() { + String text; + int from = SleepTimerPreferences.autoEnableFrom(); + int to = SleepTimerPreferences.autoEnableTo(); + + if (from == to) { + text = getString(R.string.auto_enable_label); + } else if (DateFormat.is24HourFormat(getContext())) { + String formattedFrom = String.format(Locale.getDefault(), "%02d:00", from); + String formattedTo = String.format(Locale.getDefault(), "%02d:00", to); + text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); + } else { + String formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s", + from % 12, from >= 12 ? "PM" : "AM"); + String formattedTo = String.format(Locale.getDefault(), "%02d:00 %s", + to % 12, to >= 12 ? "PM" : "AM"); + text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); + + } + chAutoEnable.setText(text); + } + @Subscribe(threadMode = ThreadMode.MAIN) @SuppressWarnings("unused") public void timerUpdated(SleepTimerUpdatedEvent event) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java index 02336949e..4e579fce4 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java @@ -77,7 +77,7 @@ public class SwipeActionsDialog { && !a.getId().equals(SwipeAction.START_DOWNLOAD)).toList(); break; case FeedItemlistFragment.TAG: - forFragment = context.getString(R.string.feeds_label); + forFragment = context.getString(R.string.individual_subscription); keys = Stream.of(keys).filter(a -> !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY)).toList(); break; case QueueFragment.TAG: diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java index d7ca80b17..64bb32897 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java @@ -7,11 +7,19 @@ import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.ArrayAdapter; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.GridLayoutManager; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.SimpleChipAdapter; import de.danoeh.antennapod.core.storage.DBReader; @@ -24,11 +32,6 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - public class TagSettingsDialog extends DialogFragment { public static final String TAG = "TagSettingsDialog"; private static final String ARG_FEED_PREFERENCES = "feed_preferences"; @@ -107,7 +110,7 @@ public class TagSettingsDialog extends DialogFragment { private void loadTags() { Observable.fromCallable( () -> { - NavDrawerData data = DBReader.getNavDrawerData(); + NavDrawerData data = DBReader.getNavDrawerData(null); List<NavDrawerData.DrawerItem> items = data.items; List<String> folders = new ArrayList<String>(); for (NavDrawerData.DrawerItem item : items) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java new file mode 100644 index 000000000..85913043e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java @@ -0,0 +1,187 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.text.format.DateFormat; +import android.view.MotionEvent; +import android.view.View; +import androidx.annotation.NonNull; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +import java.util.Locale; + +public class TimeRangeDialog extends MaterialAlertDialogBuilder { + private final TimeRangeView view; + + public TimeRangeDialog(@NonNull Context context, int from, int to) { + super(context); + view = new TimeRangeView(context, from, to); + setView(view); + setPositiveButton(android.R.string.ok, null); + } + + public int getFrom() { + return view.from; + } + + public int getTo() { + return view.to; + } + + static class TimeRangeView extends View { + private static final int DIAL_ALPHA = 120; + private final Paint paintDial = new Paint(); + private final Paint paintSelected = new Paint(); + private final Paint paintText = new Paint(); + private int from; + private int to; + private final RectF bounds = new RectF(); + int touching = 0; + + public TimeRangeView(Context context) { // Used by Android tools + this(context, 0, 0); + } + + public TimeRangeView(Context context, int from, int to) { + super(context); + this.from = from; + this.to = to; + setup(); + } + + private void setup() { + paintDial.setAntiAlias(true); + paintDial.setStyle(Paint.Style.STROKE); + paintDial.setStrokeCap(Paint.Cap.ROUND); + paintDial.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); + paintDial.setAlpha(DIAL_ALPHA); + + paintSelected.setAntiAlias(true); + paintSelected.setStyle(Paint.Style.STROKE); + paintSelected.setStrokeCap(Paint.Cap.ROUND); + paintSelected.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent)); + + paintText.setAntiAlias(true); + paintText.setStyle(Paint.Style.FILL); + paintText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); + paintText.setTextAlign(Paint.Align.CENTER); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY + && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } else if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(heightMeasureSpec, heightMeasureSpec); + } else if (MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec)) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } else { + super.onMeasure(heightMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float size = getHeight(); // square + float padding = size * 0.1f; + paintDial.setStrokeWidth(size * 0.005f); + bounds.set(padding, padding, size - padding, size - padding); + + paintText.setAlpha(DIAL_ALPHA); + canvas.drawArc(bounds, 0, 360, false, paintDial); + for (int i = 0; i < 24; i++) { + paintDial.setStrokeWidth(size * 0.005f); + if (i % 6 == 0) { + paintDial.setStrokeWidth(size * 0.01f); + Point textPos = radToPoint(i / 24.0f * 360.f, size / 2 - 2.5f * padding); + paintText.setTextSize(0.4f * padding); + canvas.drawText(String.valueOf(i), textPos.x, + textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText); + } + Point outer = radToPoint(i / 24.0f * 360.f, size / 2 - 1.7f * padding); + Point inner = radToPoint(i / 24.0f * 360.f, size / 2 - 1.9f * padding); + canvas.drawLine(outer.x, outer.y, inner.x, inner.y, paintDial); + } + paintText.setAlpha(255); + + float angleFrom = (float) from / 24 * 360 - 90; + float angleDistance = (float) ((to - from + 24) % 24) / 24 * 360; + paintSelected.setStrokeWidth(padding / 6); + paintSelected.setStyle(Paint.Style.STROKE); + canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected); + paintSelected.setStyle(Paint.Style.FILL); + Point p1 = radToPoint(angleFrom + 90, size / 2 - padding); + canvas.drawCircle(p1.x, p1.y, padding / 2, paintSelected); + Point p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding); + canvas.drawCircle(p2.x, p2.y, padding / 2, paintSelected); + + paintText.setTextSize(0.6f * padding); + String timeRange; + if (from == to) { + timeRange = getContext().getString(R.string.sleep_timer_always); + } else if (DateFormat.is24HourFormat(getContext())) { + timeRange = String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to); + } else { + timeRange = String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12, + from >= 12 ? "PM" : "AM", to % 12, to >= 12 ? "PM" : "AM"); + } + canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText); + } + + protected Point radToPoint(float angle, float radius) { + return new Point((int) (getWidth() / 2 + radius * Math.sin(-angle * Math.PI / 180 + Math.PI)), + (int) (getHeight() / 2 + radius * Math.cos(-angle * Math.PI / 180 + Math.PI))); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + getParent().requestDisallowInterceptTouchEvent(true); + Point center = new Point(getWidth() / 2, getHeight() / 2); + double angleRad = Math.atan2(center.y - event.getY(), center.x - event.getX()); + float angle = (float) (angleRad * (180 / Math.PI)); + angle += 360 + 360 - 90; + angle %= 360; + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + float fromDistance = Math.abs(angle - (float) from / 24 * 360); + float toDistance = Math.abs(angle - (float) to / 24 * 360); + if (fromDistance < 15 || fromDistance > (360 - 15)) { + touching = 1; + return true; + } else if (toDistance < 15 || toDistance > (360 - 15)) { + touching = 2; + return true; + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + int newTime = (int) (24 * (angle / 360.0)); + if (from == to && touching != 0) { + // Switch which handle is focussed such that selection is the smaller arc + touching = (((newTime - to + 24) % 24) < 12) ? 2 : 1; + } + if (touching == 1) { + from = newTime; + invalidate(); + return true; + } else if (touching == 2) { + to = newTime; + invalidate(); + return true; + } + } else if (touching != 0) { + touching = 0; + return true; + } + return super.onTouchEvent(event); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index dbf314876..7061a69f3 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -9,13 +9,17 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.dialog.AllEpisodesFilterDialog; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.HashSet; +import java.util.List; /** * Shows all episodes (possibly filtered by user). @@ -24,20 +28,53 @@ public class AllEpisodesFragment extends EpisodesListFragment { public static final String TAG = "EpisodesFragment"; private static final String PREF_NAME = "PrefAllEpisodesFragment"; private static final String PREF_FILTER = "filter"; + public static final String PREF_SORT = "prefEpisodesSort"; + private SharedPreferences prefs; @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View root = super.onCreateView(inflater, container, savedInstanceState); toolbar.inflateMenu(R.menu.episodes); + inflateSortMenu(); toolbar.setTitle(R.string.episodes_label); updateToolbar(); updateFilterUi(); + prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); txtvInformation.setOnClickListener( v -> AllEpisodesFilterDialog.newInstance(getFilter()).show(getChildFragmentManager(), null)); return root; } + private void inflateSortMenu() { + MenuItem sortItem = toolbar.getMenu().findItem(R.id.episodes_sort); + getActivity().getMenuInflater().inflate(R.menu.sort_menu, sortItem.getSubMenu()); + + // Remove the sorting options that are not needed in this fragment + toolbar.getMenu().findItem(R.id.sort_episode_title).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_feed_title).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_random).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_smart_shuffle).setVisible(false); + toolbar.getMenu().findItem(R.id.keep_sorted).setVisible(false); + } + + @NonNull + @Override + protected List<FeedItem> loadData() { + return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), getSortOrder()); + } + + @NonNull + @Override + protected List<FeedItem> loadMoreData(int page) { + return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter(), getSortOrder()); + } + + @Override + protected int loadTotalItemCount() { + return DBReader.getTotalEpisodeCount(getFilter()); + } + @Override protected FeedItemFilter getFilter() { SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); @@ -71,13 +108,23 @@ public class AllEpisodesFragment extends EpisodesListFragment { } onFilterChanged(new AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent(new HashSet<>(filter))); return true; + } else { + SortOrder sortOrder = MenuItemToSortOrderConverter.convert(item); + if (sortOrder != null) { + saveSortOrderAndRefresh(sortOrder); + return true; + } } return false; } + private void saveSortOrderAndRefresh(SortOrder type) { + prefs.edit().putString(PREF_SORT, "" + type.code).apply(); + loadItems(); + } + @Subscribe public void onFilterChanged(AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent event) { - SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); prefs.edit().putString(PREF_FILTER, StringUtils.join(event.filterValues, ",")).apply(); updateFilterUi(); page = 1; @@ -96,4 +143,8 @@ public class AllEpisodesFragment extends EpisodesListFragment { toolbar.getMenu().findItem(R.id.action_favorites).setIcon( getFilter().showIsFavorite ? R.drawable.ic_star : R.drawable.ic_star_border); } + + private SortOrder getSortOrder() { + return SortOrder.fromCodeString(prefs.getString(PREF_SORT, "" + SortOrder.DATE_NEW_OLD.code)); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index e93db2d2a..e5aac5fa5 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -15,7 +15,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.appbar.MaterialToolbar; import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; @@ -25,10 +24,10 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.elevation.SurfaceColors; -import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog; import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; import de.danoeh.antennapod.event.PlayerErrorEvent; @@ -396,19 +395,7 @@ public class AudioPlayerFragment extends Fragment implements @Subscribe(threadMode = ThreadMode.MAIN) public void mediaPlayerError(PlayerErrorEvent event) { - final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(getContext()); - errorDialog.setTitle(R.string.error_label); - errorDialog.setMessage(event.getMessage()); - errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) -> - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); - if (!UserPreferences.useExoplayer()) { - errorDialog.setNeutralButton(R.string.media_player_switch_to_exoplayer, (dialog, which) -> { - UserPreferences.enableExoplayer(); - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.media_player_switched_to_exoplayer, Snackbar.LENGTH_LONG); - }); - } - errorDialog.create().show(); + MediaPlayerErrorDialog.show(getActivity(), event); } @Override @@ -528,7 +515,7 @@ public class AudioPlayerFragment extends Fragment implements float playerFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; View player = getView().findViewById(R.id.playerFragment); player.setAlpha(1 - playerFadeProgress); - player.setVisibility(playerFadeProgress > 0.99f ? View.GONE : View.VISIBLE); + player.setVisibility(playerFadeProgress > 0.99f ? View.INVISIBLE : View.VISIBLE); float toolbarFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.6f)) / 0.2f; toolbar.setAlpha(toolbarFadeProgress); toolbar.setVisibility(toolbarFadeProgress < 0.01f ? View.INVISIBLE : View.VISIBLE); 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 809ca96c5..ae18fecda 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ProgressBar; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -158,8 +159,9 @@ public class ChaptersFragment extends AppCompatDialogFragment { if (adapter == null) { return; } - if (media.getChapters() != null && media.getChapters().size() <= 0) { + if (media.getChapters() != null && media.getChapters().size() == 0) { dismiss(); + Toast.makeText(getContext(), R.string.no_chapters_label, Toast.LENGTH_LONG).show(); } else { progressBar.setVisibility(View.GONE); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index b39b41897..737975389 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -4,6 +4,8 @@ import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -23,7 +25,7 @@ import de.danoeh.antennapod.core.event.DownloadLogEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; @@ -33,6 +35,8 @@ import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.view.EmptyViewHandler; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.LiftOnScrollListener; @@ -69,14 +73,17 @@ public class CompletedDownloadsFragment extends Fragment private SpeedDialView speedDialView; private SwipeActions swipeActions; private ProgressBar progressBar; + private MaterialToolbar toolbar; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.simple_list_fragment, container, false); - MaterialToolbar toolbar = root.findViewById(R.id.toolbar); + toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(R.string.downloads_label); toolbar.inflateMenu(R.menu.downloads_completed); + inflateSortMenu(toolbar); + toolbar.setOnMenuItemClickListener(this); toolbar.setOnLongClickListener(v -> { recyclerView.scrollToPosition(5); @@ -139,6 +146,19 @@ public class CompletedDownloadsFragment extends Fragment return root; } + private void inflateSortMenu(MaterialToolbar toolbar) { + Menu menu = toolbar.getMenu(); + MenuItem downloadsItem = menu.findItem(R.id.downloads_sort); + MenuInflater menuInflater = getActivity().getMenuInflater(); + menuInflater.inflate(R.menu.sort_menu, downloadsItem.getSubMenu()); + + // Remove the sorting options that are not needed in this fragment + menu.findItem(R.id.sort_feed_title).setVisible(false); + menu.findItem(R.id.sort_random).setVisible(false); + menu.findItem(R.id.sort_smart_shuffle).setVisible(false); + menu.findItem(R.id.keep_sorted).setVisible(false); + } + @Override public void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(KEY_UP_ARROW, displayUpArrow); @@ -149,6 +169,10 @@ public class CompletedDownloadsFragment extends Fragment public void onDestroyView() { EventBus.getDefault().unregister(this); adapter.endSelectMode(); + if (toolbar != null) { + toolbar.setOnMenuItemClickListener(null); + toolbar.setOnLongClickListener(null); + } super.onDestroyView(); } @@ -169,7 +193,7 @@ public class CompletedDownloadsFragment extends Fragment @Override public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); return true; } else if (item.getItemId() == R.id.action_download_logs) { new DownloadLogFragment().show(getChildFragmentManager(), null); @@ -177,10 +201,21 @@ public class CompletedDownloadsFragment extends Fragment } else if (item.getItemId() == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); return true; + } else { + SortOrder sortOrder = MenuItemToSortOrderConverter.convert(item); + if (sortOrder != null) { + setSortOrder(sortOrder); + return true; + } } return false; } + private void setSortOrder(SortOrder sortOrder) { + UserPreferences.setDownloadsSortedOrder(sortOrder); + loadItems(); + } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); @@ -279,7 +314,10 @@ public class CompletedDownloadsFragment extends Fragment } emptyView.hide(); disposable = Observable.fromCallable(() -> { - List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); + List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder); + List<Long> mediaIds = new ArrayList<>(); if (runningDownloads == null) { return downloadedItems; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java index 88b9ac8f1..16ccb2af4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java @@ -14,17 +14,25 @@ import android.widget.Button; import android.widget.GridView; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; - import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputLayout; - +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent; +import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; @@ -35,20 +43,13 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.OnlineFeedViewActivity; -import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; -import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent; -import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; -import de.danoeh.antennapod.net.discovery.PodcastSearchResult; -import io.reactivex.disposables.Disposable; - /** * Searches iTunes store for top podcasts and displays results in a list. */ public class DiscoveryFragment extends Fragment implements Toolbar.OnMenuItemClickListener { private static final String TAG = "ItunesSearchFragment"; + private static final int NUM_OF_TOP_PODCASTS = 25; private SharedPreferences prefs; /** @@ -188,19 +189,23 @@ public class DiscoveryFragment extends Fragment implements Toolbar.OnMenuItemCli } ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); - disposable = loader.loadToplist(country, 25).subscribe( - podcasts -> { - progressBar.setVisibility(View.GONE); - topList = podcasts; - updateData(topList); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - txtvError.setText(error.getMessage()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setOnClickListener(v -> loadToplist(country)); - butRetry.setVisibility(View.VISIBLE); - }); + disposable = Observable.fromCallable(() -> + loader.loadToplist(country, NUM_OF_TOP_PODCASTS, DBReader.getFeedList())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + progressBar.setVisibility(View.GONE); + topList = podcasts; + updateData(topList); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> loadToplist(country)); + butRetry.setVisibility(View.VISIBLE); + }); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index f5221d47b..ef784e9ee 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -30,12 +30,11 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; @@ -124,7 +123,7 @@ public abstract class EpisodesListFragment extends Fragment } final int itemId = item.getItemId(); if (itemId == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); @@ -185,7 +184,7 @@ public abstract class EpisodesListFragment extends Fragment SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); }); @@ -445,18 +444,12 @@ public abstract class EpisodesListFragment extends Fragment } @NonNull - protected List<FeedItem> loadData() { - return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, getFilter()); - } + protected abstract List<FeedItem> loadData(); @NonNull - protected List<FeedItem> loadMoreData(int page) { - return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter()); - } + protected abstract List<FeedItem> loadMoreData(int page); - protected int loadTotalItemCount() { - return DBReader.getTotalEpisodeCount(getFilter()); - } + protected abstract int loadTotalItemCount(); protected abstract FeedItemFilter getFilter(); @@ -465,9 +458,12 @@ public abstract class EpisodesListFragment extends Fragment protected abstract String getPrefName(); protected void updateToolbar() { + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { if (toolbar.getMenu().findItem(R.id.refresh_item) != null) { - MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, - DownloadService.isRunning && DownloadService.isDownloadingFeeds()); + MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, event.isFeedUpdateRunning); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index 32ab5d9e5..930440b39 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -196,7 +196,7 @@ public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenu Log.d(TAG, "Language is " + feed.getLanguage()); Log.d(TAG, "Author is " + feed.getAuthor()); Log.d(TAG, "URL is " + feed.getDownload_url()); - Glide.with(getContext()) + Glide.with(this) .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) @@ -204,7 +204,7 @@ public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenu .fitCenter() .dontAnimate()) .into(imgvCover); - Glide.with(getContext()) + Glide.with(this) .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 0b264b5a3..61883afe7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -33,11 +33,11 @@ import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.databinding.FeedItemListFragmentBinding; import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; @@ -48,6 +48,7 @@ import de.danoeh.antennapod.dialog.RenameItemDialog; import de.danoeh.antennapod.event.FavoritesEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.QueueEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; @@ -60,6 +61,7 @@ import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.glide.FastBlurTransformation; import de.danoeh.antennapod.view.ToolbarIconTintManager; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; @@ -164,7 +166,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem nextPageLoader = new MoreContentListFooterUtil(viewBinding.moreContent.moreContentListFooter); nextPageLoader.setClickListener(() -> { if (feed != null) { - DBTasks.loadNextPageOfFeed(getActivity(), feed, false); + FeedUpdateManager.runOnce(getContext(), feed, true); } }); viewBinding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -241,8 +243,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null); - MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(), R.id.refresh_item, - DownloadService.isRunning && DownloadService.isDownloadingFile(feed.getDownload_url())); FeedMenuHandler.onPrepareOptionsMenu(viewBinding.toolbar.getMenu(), feed); } @@ -271,8 +271,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem new RenameItemDialog(getActivity(), feed).show(); return true; } else if (itemId == R.id.remove_feed) { - ((MainActivity) getActivity()).loadFragment(AllEpisodesFragment.TAG, null); - RemoveFeedDialog.show(getContext(), feed); + RemoveFeedDialog.show(getContext(), feed, () -> { + ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); + // Make sure fragment is hidden before actually starting to delete + getActivity().getSupportFragmentManager().executePendingTransactions(); + }); return true; } else if (itemId == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle())); @@ -384,7 +387,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private void updateUi() { loadItems(); - updateSyncProgressBarVisibility(); } @Subscribe(threadMode = ThreadMode.MAIN) @@ -404,12 +406,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } } - private void updateSyncProgressBarVisibility() { - updateToolbar(); - if (!DownloadService.isDownloadingFeeds()) { + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + nextPageLoader.setLoadingState(event.isFeedUpdateRunning); + if (!event.isFeedUpdateRunning) { nextPageLoader.getRoot().setVisibility(View.GONE); } - nextPageLoader.setLoadingState(DownloadService.isDownloadingFeeds()); + MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(), + R.id.refresh_item, event.isFeedUpdateRunning); } private void refreshHeaderView() { @@ -534,14 +538,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem adapter.setDummyViews(0); adapter.updateItems(feed.getItems()); updateToolbar(); - updateSyncProgressBarVisibility(); }, error -> { feed = null; refreshHeaderView(); adapter.setDummyViews(0); adapter.updateItems(Collections.emptyList()); updateToolbar(); - updateSyncProgressBarVisibility(); Log.e(TAG, Log.getStackTraceString(error)); }); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 47c2e4dcd..ae9e003d5 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -16,7 +16,7 @@ import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; @@ -105,6 +105,7 @@ public class FeedSettingsFragment extends Fragment { private static final CharSequence PREF_AUTHENTICATION = "authentication"; private static final CharSequence PREF_AUTO_DELETE = "autoDelete"; private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory"; + private static final CharSequence PREF_NEW_EPISODES_ACTION = "feedNewEpisodesAction"; private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed"; private static final String PREF_AUTO_SKIP = "feedAutoSkip"; private static final String PREF_TAGS = "tags"; @@ -156,6 +157,7 @@ public class FeedSettingsFragment extends Fragment { setupKeepUpdatedPreference(); setupAutoDeletePreference(); setupVolumeReductionPreferences(); + setupNewEpisodesAction(); setupAuthentificationPreference(); setupEpisodeFilterPreference(); setupPlaybackSpeedPreference(); @@ -166,6 +168,7 @@ public class FeedSettingsFragment extends Fragment { updateAutoDeleteSummary(); updateVolumeReductionValue(); updateAutoDownloadEnabled(); + updateNewEpisodesAction(); if (feed.isLocalFeed()) { findPreference(PREF_AUTHENTICATION).setVisible(false); @@ -267,8 +270,7 @@ public class FeedSettingsFragment extends Fragment { } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } - - DBTasks.forceRefreshFeed(requireContext(), feed, true); + FeedUpdateManager.runOnce(getContext(), feed); }, "RefreshAfterCredentialChange").start(); } }.show(); @@ -283,11 +285,12 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.GLOBAL); break; case "always": - feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.YES); + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.ALWAYS); break; case "never": - feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NEVER); break; + default: } DBWriter.setFeedPreferences(feedPreferences); updateAutoDeleteSummary(); @@ -300,14 +303,14 @@ public class FeedSettingsFragment extends Fragment { switch (feedPreferences.getAutoDeleteAction()) { case GLOBAL: - autoDeletePreference.setSummary(R.string.feed_auto_download_global); + autoDeletePreference.setSummary(R.string.global_default); autoDeletePreference.setValue("global"); break; - case YES: + case ALWAYS: autoDeletePreference.setSummary(R.string.feed_auto_download_always); autoDeletePreference.setValue("always"); break; - case NO: + case NEVER: autoDeletePreference.setSummary(R.string.feed_auto_download_never); autoDeletePreference.setValue("never"); break; @@ -327,6 +330,7 @@ public class FeedSettingsFragment extends Fragment { case "heavy": feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); break; + default: } DBWriter.setFeedPreferences(feedPreferences); updateVolumeReductionValue(); @@ -352,6 +356,34 @@ public class FeedSettingsFragment extends Fragment { } } + private void setupNewEpisodesAction() { + findPreference(PREF_NEW_EPISODES_ACTION).setOnPreferenceChangeListener((preference, newValue) -> { + int code = Integer.parseInt((String) newValue); + feedPreferences.setNewEpisodesAction(FeedPreferences.NewEpisodesAction.fromCode(code)); + DBWriter.setFeedPreferences(feedPreferences); + updateNewEpisodesAction(); + return false; + }); + } + + private void updateNewEpisodesAction() { + ListPreference newEpisodesAction = findPreference(PREF_NEW_EPISODES_ACTION); + newEpisodesAction.setValue("" + feedPreferences.getNewEpisodesAction().code); + + switch (feedPreferences.getNewEpisodesAction()) { + case GLOBAL: + newEpisodesAction.setSummary(R.string.global_default); + break; + case ADD_TO_INBOX: + newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_inbox); + break; + case NOTHING: + newEpisodesAction.setSummary(R.string.feed_new_episodes_action_nothing); + break; + default: + } + } + private void setupKeepUpdatedPreference() { SwitchPreferenceCompat pref = findPreference("keepUpdated"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java index 37cd40b0e..f8bcbb532 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java @@ -4,6 +4,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -20,6 +22,8 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import java.util.List; @@ -38,6 +42,8 @@ public class InboxFragment extends EpisodesListFragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View root = super.onCreateView(inflater, container, savedInstanceState); toolbar.inflateMenu(R.menu.inbox); + inflateSortMenu(); + toolbar.setTitle(R.string.inbox_label); prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); updateToolbar(); @@ -77,6 +83,13 @@ public class InboxFragment extends EpisodesListFragment { showRemoveAllDialog(); } return true; + } else { + SortOrder sortOrder = MenuItemToSortOrderConverter.convert(item); + if (sortOrder != null) { + UserPreferences.setInboxSortedOrder(sortOrder); + loadItems(); + return true; + } } return false; } @@ -84,13 +97,15 @@ public class InboxFragment extends EpisodesListFragment { @NonNull @Override protected List<FeedItem> loadData() { - return DBReader.getNewItemsList(0, page * EPISODES_PER_PAGE); + return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); } @NonNull @Override protected List<FeedItem> loadMoreData(int page) { - return DBReader.getNewItemsList((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE); + return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); } @Override @@ -103,6 +118,20 @@ public class InboxFragment extends EpisodesListFragment { ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.removed_all_inbox_msg, Toast.LENGTH_SHORT); } + private void inflateSortMenu() { + Menu menu = toolbar.getMenu(); + MenuItem downloadsItem = menu.findItem(R.id.inbox_sort); + MenuInflater menuInflater = getActivity().getMenuInflater(); + menuInflater.inflate(R.menu.sort_menu, downloadsItem.getSubMenu()); + + // Remove the sorting options that are not needed in this fragment + toolbar.getMenu().findItem(R.id.sort_episode_title).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_feed_title).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_random).setVisible(false); + toolbar.getMenu().findItem(R.id.sort_smart_shuffle).setVisible(false); + toolbar.getMenu().findItem(R.id.keep_sorted).setVisible(false); + } + private void showRemoveAllDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); builder.setTitle(R.string.remove_all_inbox_label); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 31a7d81fb..df48c9a98 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -297,9 +297,9 @@ public class ItemFragment extends Fragment { new RoundedCorners((int) (8 * getResources().getDisplayMetrics().density))) .dontAnimate(); - Glide.with(getActivity()) + Glide.with(this) .load(item.getImageLocation()) - .error(Glide.with(getActivity()) + .error(Glide.with(this) .load(ImageResourceUtils.getFallbackImageLocation(item)) .apply(options)) .apply(options) 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 762d919f4..f2cd02cec 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java @@ -48,7 +48,7 @@ public class ItemPagerFragment extends Fragment implements MaterialToolbar.OnMen ItemPagerFragment fragment = new ItemPagerFragment(); Bundle args = new Bundle(); args.putLongArray(ARG_FEEDITEMS, feeditems); - args.putInt(ARG_FEEDITEM_POS, feedItemPos); + args.putInt(ARG_FEEDITEM_POS, Math.max(0, feedItemPos)); fragment.setArguments(args); return fragment; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/MenuItemToSortOrderConverter.java b/app/src/main/java/de/danoeh/antennapod/fragment/MenuItemToSortOrderConverter.java new file mode 100644 index 000000000..51911b122 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/MenuItemToSortOrderConverter.java @@ -0,0 +1,40 @@ +package de.danoeh.antennapod.fragment; + +import android.view.MenuItem; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.SortOrder; + +public class MenuItemToSortOrderConverter { + + public static SortOrder convert(MenuItem item) { + final int itemId = item.getItemId(); + + if (itemId == R.id.sort_episode_title_asc) { + return SortOrder.EPISODE_TITLE_A_Z; + } else if (itemId == R.id.sort_episode_title_desc) { + return SortOrder.EPISODE_TITLE_Z_A; + } else if (itemId == R.id.sort_date_asc) { + return SortOrder.DATE_OLD_NEW; + } else if (itemId == R.id.sort_date_desc) { + return SortOrder.DATE_NEW_OLD; + } else if (itemId == R.id.sort_duration_asc) { + return SortOrder.DURATION_SHORT_LONG; + } else if (itemId == R.id.sort_duration_desc) { + return SortOrder.DURATION_LONG_SHORT; + } else if (itemId == R.id.sort_feed_title_asc) { + return SortOrder.FEED_TITLE_A_Z; + } else if (itemId == R.id.sort_feed_title_desc) { + return SortOrder.FEED_TITLE_Z_A; + } else if (itemId == R.id.sort_random) { + return SortOrder.RANDOM; + } else if (itemId == R.id.sort_smart_shuffle_asc) { + return SortOrder.SMART_SHUFFLE_OLD_NEW; + } else if (itemId == R.id.sort_smart_shuffle_desc) { + return SortOrder.SMART_SHUFFLE_NEW_OLD; + } + + return null; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 7c3ed4f2f..9f3e6465f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -13,6 +13,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -20,14 +21,26 @@ import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + import com.google.android.material.bottomsheet.BottomSheetBehavior; + +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.NavDrawerData; @@ -40,21 +53,13 @@ import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.QueueEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.home.HomeFragment; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; public class NavDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener { @VisibleForTesting @@ -174,8 +179,13 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS new RenameItemDialog(getActivity(), feed).show(); return true; } else if (itemId == R.id.remove_feed) { - ((MainActivity) getActivity()).loadFragment(AllEpisodesFragment.TAG, null); - RemoveFeedDialog.show(getContext(), feed); + RemoveFeedDialog.show(getContext(), feed, () -> { + if (String.valueOf(feed.getId()).equals(getLastNavFragment(getContext()))) { + ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); + // Make sure fragment is hidden before actually starting to delete + getActivity().getSupportFragmentManager().executePendingTransactions(); + } + }); return true; } return super.onContextItemSelected(item); @@ -338,7 +348,15 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS @Override public boolean onItemLongClick(int position) { if (position < navAdapter.getFragmentTags().size()) { - DrawerPreferencesDialog.show(getContext(), () -> navAdapter.notifyDataSetChanged()); + DrawerPreferencesDialog.show(getContext(), () -> { + navAdapter.notifyDataSetChanged(); + if (UserPreferences.getHiddenDrawerItems().contains(getLastNavFragment(getContext()))) { + new MainActivityStarter(getContext()) + .withFragmentLoaded(UserPreferences.getDefaultPage()) + .withDrawerOpen() + .start(); + } + }); return true; } else { contextPressedItem = flatItemList.get(position - navAdapter.getSubscriptionOffset()); @@ -355,7 +373,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS private void loadData() { disposable = Observable.fromCallable( () -> { - NavDrawerData data = DBReader.getNavDrawerData(); + NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()); return new Pair<>(data, makeFlatDrawerData(data.items, 0)); }) .subscribeOn(Schedulers.io()) 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 12fa20d05..6681df4c1 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -10,6 +10,7 @@ import android.util.Log; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -17,13 +18,13 @@ import android.widget.CheckBox; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.appbar.MaterialToolbar; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.leinardi.android.speeddial.SpeedDialView; import de.danoeh.antennapod.R; @@ -35,14 +36,13 @@ import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.QueueEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; @@ -53,6 +53,7 @@ import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.view.EmptyViewHandler; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.LiftOnScrollListener; @@ -250,16 +251,23 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte recyclerAdapter.endSelectMode(); } recyclerAdapter = null; + if (toolbar != null) { + toolbar.setOnMenuItemClickListener(null); + toolbar.setOnLongClickListener(null); + } } private void refreshToolbarState() { boolean keepSorted = UserPreferences.isQueueKeepSorted(); toolbar.getMenu().findItem(R.id.queue_lock).setChecked(UserPreferences.isQueueLocked()); toolbar.getMenu().findItem(R.id.queue_lock).setVisible(!keepSorted); - toolbar.getMenu().findItem(R.id.queue_sort_random).setVisible(!keepSorted); - toolbar.getMenu().findItem(R.id.queue_keep_sorted).setChecked(keepSorted); - MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), - R.id.refresh_item, DownloadService.isRunning && DownloadService.isDownloadingFeeds()); + toolbar.getMenu().findItem(R.id.sort_random).setVisible(!keepSorted); + toolbar.getMenu().findItem(R.id.keep_sorted).setChecked(keepSorted); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, event.isFeedUpdateRunning); } @Override @@ -269,7 +277,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte toggleQueueLock(); return true; } else if (itemId == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.clear_queue) { // make sure the user really wants to clear the queue @@ -286,40 +294,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte }; conDialog.createNewDialog().show(); return true; - } else if (itemId == R.id.queue_sort_episode_title_asc) { - setSortOrder(SortOrder.EPISODE_TITLE_A_Z); - return true; - } else if (itemId == R.id.queue_sort_episode_title_desc) { - setSortOrder(SortOrder.EPISODE_TITLE_Z_A); - return true; - } else if (itemId == R.id.queue_sort_date_asc) { - setSortOrder(SortOrder.DATE_OLD_NEW); - return true; - } else if (itemId == R.id.queue_sort_date_desc) { - setSortOrder(SortOrder.DATE_NEW_OLD); - return true; - } else if (itemId == R.id.queue_sort_duration_asc) { - setSortOrder(SortOrder.DURATION_SHORT_LONG); - return true; - } else if (itemId == R.id.queue_sort_duration_desc) { - setSortOrder(SortOrder.DURATION_LONG_SHORT); - return true; - } else if (itemId == R.id.queue_sort_feed_title_asc) { - setSortOrder(SortOrder.FEED_TITLE_A_Z); - return true; - } else if (itemId == R.id.queue_sort_feed_title_desc) { - setSortOrder(SortOrder.FEED_TITLE_Z_A); - return true; - } else if (itemId == R.id.queue_sort_random) { - setSortOrder(SortOrder.RANDOM); - return true; - } else if (itemId == R.id.queue_sort_smart_shuffle_asc) { - setSortOrder(SortOrder.SMART_SHUFFLE_OLD_NEW); - return true; - } else if (itemId == R.id.queue_sort_smart_shuffle_desc) { - setSortOrder(SortOrder.SMART_SHUFFLE_NEW_OLD); - return true; - } else if (itemId == R.id.queue_keep_sorted) { + } else if (itemId == R.id.keep_sorted) { boolean keepSortedOld = UserPreferences.isQueueKeepSorted(); boolean keepSortedNew = !keepSortedOld; UserPreferences.setQueueKeepSorted(keepSortedNew); @@ -335,6 +310,12 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte } else if (itemId == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); return true; + } else { + SortOrder sortOrder = MenuItemToSortOrderConverter.convert(item); + if (sortOrder != null) { + setSortOrder(sortOrder); + return true; + } } return false; } @@ -444,6 +425,10 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte } ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.queue); + + MenuItem queueItem = toolbar.getMenu().findItem(R.id.queue_sort); + MenuInflater menuInflater = getActivity().getMenuInflater(); + menuInflater.inflate(R.menu.sort_menu, queueItem.getSubMenu()); refreshToolbarState(); progressBar = root.findViewById(R.id.progressBar); progressBar.setVisibility(View.VISIBLE); @@ -475,7 +460,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); }); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java index acdd1e1c7..d5192061c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java @@ -5,7 +5,6 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import android.util.DisplayMetrics; -import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -16,20 +15,23 @@ import android.widget.Button; import android.widget.GridView; import android.widget.LinearLayout; import android.widget.TextView; - +import androidx.fragment.app.Fragment; import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; -import de.danoeh.antennapod.net.discovery.PodcastSearchResult; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.adapter.FeedDiscoverAdapter; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent; +import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; import java.util.List; @@ -138,26 +140,29 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView. return; } - disposable = loader.loadToplist(countryCode, NUM_SUGGESTIONS) + disposable = Observable.fromCallable(() -> + loader.loadToplist(countryCode, NUM_SUGGESTIONS, DBReader.getFeedList())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) .subscribe( - podcasts -> { - errorView.setVisibility(View.GONE); - if (podcasts.size() == 0) { - errorTextView.setText(getResources().getText(R.string.search_status_no_results)); - errorView.setVisibility(View.VISIBLE); - discoverGridLayout.setVisibility(View.INVISIBLE); - } else { - discoverGridLayout.setVisibility(View.VISIBLE); - adapter.updateData(podcasts); - } - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - errorTextView.setText(error.getLocalizedMessage()); + podcasts -> { + errorView.setVisibility(View.GONE); + if (podcasts.size() == 0) { + errorTextView.setText(getResources().getText(R.string.search_status_no_results)); errorView.setVisibility(View.VISIBLE); discoverGridLayout.setVisibility(View.INVISIBLE); - errorRetry.setVisibility(View.VISIBLE); - errorRetry.setOnClickListener((listener) -> loadToplist()); - }); + } else { + discoverGridLayout.setVisibility(View.VISIBLE); + adapter.updateData(podcasts); + } + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + errorTextView.setText(error.getLocalizedMessage()); + errorView.setVisibility(View.VISIBLE); + discoverGridLayout.setVisibility(View.INVISIBLE); + errorRetry.setVisibility(View.VISIBLE); + errorRetry.setOnClickListener(v -> loadToplist()); + }); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index 77fec7a8f..d2aa35549 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -54,6 +54,9 @@ import org.greenrobot.eventbus.ThreadMode; import java.util.Collections; import java.util.List; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; +import de.danoeh.antennapod.event.FeedListUpdateEvent; + /** * Performs a search operation on all feeds or one specific feed and displays the search result. @@ -132,6 +135,7 @@ public class SearchFragment extends Fragment { recyclerView = layout.findViewById(R.id.recyclerView); recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + registerForContextMenu(recyclerView); adapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { @@ -146,7 +150,14 @@ public class SearchFragment extends Fragment { LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity()); layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL); recyclerViewFeeds.setLayoutManager(layoutManagerFeeds); - adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity()); + adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + super.onCreateContextMenu(contextMenu, view, contextMenuInfo); + MenuItemUtils.setOnClickListeners(contextMenu, SearchFragment.this::onContextItemSelected); + } + }; recyclerViewFeeds.setAdapter(adapterFeeds); emptyViewHandler = new EmptyViewHandler(getContext()); @@ -241,12 +252,21 @@ public class SearchFragment extends Fragment { @Override public boolean onContextItemSelected(@NonNull MenuItem item) { + Feed selectedFeedItem = adapterFeeds.getLongPressedItem(); + if (selectedFeedItem != null + && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeedItem, () -> { })) { + return true; + } FeedItem selectedItem = adapter.getLongPressedItem(); - if (selectedItem == null) { - Log.i(TAG, "Selected item at current position was null, ignoring selection"); - return super.onContextItemSelected(item); + if (selectedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem)) { + return true; } - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + return super.onContextItemSelected(item); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + search(); } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index 2800537e2..490c79d47 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.fragment; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; @@ -17,51 +15,45 @@ import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import com.google.android.material.appbar.MaterialToolbar; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.joanzapata.iconify.Iconify; import com.leinardi.android.speeddial.SpeedDialView; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.ui.statistics.StatisticsFragment; -import de.danoeh.antennapod.view.LiftOnScrollListener; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.concurrent.Callable; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.NavDrawerData; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.FeedSortDialog; -import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.RenameItemDialog; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.fragment.actions.FeedMultiSelectActionHandler; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; import de.danoeh.antennapod.view.EmptyViewHandler; +import de.danoeh.antennapod.view.LiftOnScrollListener; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -150,7 +142,6 @@ public class SubscriptionFragment extends Fragment } subscriptionRecycler = root.findViewById(R.id.subscriptions_grid); - setColumnNumber(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns())); subscriptionRecycler.addItemDecoration(new SubscriptionsRecyclerAdapter.GridDividerItemDecorator()); registerForContextMenu(subscriptionRecycler); subscriptionRecycler.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); @@ -161,6 +152,7 @@ public class SubscriptionFragment extends Fragment MenuItemUtils.setOnClickListeners(menu, SubscriptionFragment.this::onContextItemSelected); } }; + setColumnNumber(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns())); subscriptionAdapter.setOnSelectModeListener(this); subscriptionRecycler.setAdapter(subscriptionAdapter); setupEmptyView(); @@ -181,7 +173,7 @@ public class SubscriptionFragment extends Fragment SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); }); @@ -217,16 +209,18 @@ public class SubscriptionFragment extends Fragment private void refreshToolbarState() { int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); + } - MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, - DownloadService.isRunning && DownloadService.isDownloadingFeeds()); + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, event.isFeedUpdateRunning); } @Override public boolean onMenuItemClick(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.subscriptions_filter) { SubscriptionsFilterDialog.showDialog(requireContext()); @@ -259,6 +253,7 @@ public class SubscriptionFragment extends Fragment private void setColumnNumber(int columns) { GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), columns, RecyclerView.VERTICAL, false); + subscriptionAdapter.setColumnCount(columns); subscriptionRecycler.setLayoutManager(gridLayoutManager); prefs.edit().putInt(PREF_NUM_COLUMNS, columns).apply(); refreshToolbarState(); @@ -298,7 +293,7 @@ public class SubscriptionFragment extends Fragment emptyView.hide(); disposable = Observable.fromCallable( () -> { - NavDrawerData data = DBReader.getNavDrawerData(); + NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()); List<NavDrawerData.DrawerItem> items = data.items; for (NavDrawerData.DrawerItem item : items) { if (item.type == NavDrawerData.DrawerItem.Type.TAG @@ -350,43 +345,11 @@ public class SubscriptionFragment extends Fragment } Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; - if (itemId == R.id.remove_all_inbox_item) { - displayConfirmationDialog( - R.string.remove_all_inbox_label, - R.string.remove_all_inbox_confirmation_msg, - () -> DBWriter.removeFeedNewFlag(feed.getId())); - return true; - } else if (itemId == R.id.edit_tags) { - TagSettingsDialog.newInstance(Collections.singletonList(feed.getPreferences())) - .show(getChildFragmentManager(), TagSettingsDialog.TAG); - return true; - } else if (itemId == R.id.rename_item) { - new RenameItemDialog(getActivity(), feed).show(); - return true; - } else if (itemId == R.id.remove_feed) { - RemoveFeedDialog.show(getContext(), feed); - return true; - } else if (itemId == R.id.multi_select) { + if (itemId == R.id.multi_select) { speedDialView.setVisibility(View.VISIBLE); return subscriptionAdapter.onContextItemSelected(item); } - return super.onContextItemSelected(item); - } - - private <T> void displayConfirmationDialog(@StringRes int title, @StringRes int message, Callable<? extends T> task) { - ConfirmationDialog dialog = new ConfirmationDialog(getActivity(), title, message) { - @Override - @SuppressLint("CheckResult") - public void onConfirmButtonPressed(DialogInterface clickedDialog) { - clickedDialog.dismiss(); - Observable.fromCallable(task) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> loadSubscriptions(), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - }; - dialog.createNewDialog().show(); + return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), feed, this::loadSubscriptions); } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java index 203a64c32..bbe78f4a2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java @@ -89,28 +89,12 @@ public class FeedMultiSelectActionHandler { private void autoDeleteEpisodesPrefHandler() { PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity, - "Auto delete episodes"); + activity.getString(R.string.auto_delete_label)); String[] items = activity.getResources().getStringArray(R.array.spnAutoDeleteItems); - String[] values = activity.getResources().getStringArray(R.array.spnAutoDeleteValues); preferenceListDialog.openDialog(items); preferenceListDialog.setOnPreferenceChangedListener(which -> { - FeedPreferences.AutoDeleteAction autoDeleteAction = null; - switch (values[which]) { - case "global": - autoDeleteAction = FeedPreferences.AutoDeleteAction.GLOBAL; - break; - case "always": - autoDeleteAction = FeedPreferences.AutoDeleteAction.YES; - break; - case "never": - autoDeleteAction = FeedPreferences.AutoDeleteAction.NO; - break; - default: - } - FeedPreferences.AutoDeleteAction finalAutoDeleteAction = autoDeleteAction; - saveFeedPreferences(feedPreferences -> { - feedPreferences.setAutoDeleteAction(finalAutoDeleteAction); - }); + FeedPreferences.AutoDeleteAction autoDeleteAction = FeedPreferences.AutoDeleteAction.fromCode(which); + saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDeleteAction(autoDeleteAction)); }); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java new file mode 100644 index 000000000..c486089fc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java @@ -0,0 +1,95 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.dialog.ChooseDataFolderDialog; +import de.danoeh.antennapod.dialog.ProxyDialog; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +import java.io.File; + + +public class DownloadsPreferencesFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; + private static final String PREF_PROXY = "prefProxy"; + private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_downloads); + setupNetworkScreen(); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.downloads_pref); + PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onResume() { + super.onResume(); + setParallelDownloadsText(UserPreferences.getParallelDownloads()); + setDataFolderText(); + } + + private void setupNetworkScreen() { + findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); + return true; + }); + findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setOnPreferenceChangeListener((preference, o) -> { + if (o instanceof Integer) { + setParallelDownloadsText((Integer) o); + } + return true; + }); + // validate and set correct value: number of downloads between 1 and 50 (inclusive) + findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { + ProxyDialog dialog = new ProxyDialog(getActivity()); + dialog.show(); + return true; + }); + findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(preference -> { + ChooseDataFolderDialog.showDialog(getContext(), path -> { + UserPreferences.setDataFolder(path); + setDataFolderText(); + }); + return true; + }); + } + + private void setParallelDownloadsText(int downloads) { + final Resources res = getActivity().getResources(); + String s = res.getString(R.string.parallel_downloads, downloads); + findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); + } + + private void setDataFolderText() { + File f = UserPreferences.getDataFolder(null); + if (f != null) { + findPreference(PREF_CHOOSE_DATA_DIR).setSummary(f.getAbsolutePath()); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { + FeedUpdateManager.restartUpdateAlarm(getContext(), true); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index 91bb00437..4d7313247 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -5,7 +5,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.os.Bundle; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; @@ -30,9 +29,9 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface"; private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback"; - private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork"; + private static final String PREF_SCREEN_DOWNLOADS = "prefScreenDownloads"; + private static final String PREF_SCREEN_IMPORT_EXPORT = "prefScreenImportExport"; private static final String PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization"; - private static final String PREF_SCREEN_STORAGE = "prefScreenStorage"; private static final String PREF_DOCUMENTATION = "prefDocumentation"; private static final String PREF_VIEW_FORUM = "prefViewForum"; private static final String PREF_SEND_BUG_REPORT = "prefSendBugReport"; @@ -40,7 +39,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_ABOUT = "prefAbout"; private static final String PREF_NOTIFICATION = "notifications"; private static final String PREF_CONTRIBUTE = "prefContribute"; - private static final String PREF_STATISTICS = "statistics"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -90,16 +88,16 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_playback); return true; }); - findPreference(PREF_SCREEN_NETWORK).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_network); + findPreference(PREF_SCREEN_DOWNLOADS).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_downloads); return true; }); findPreference(PREF_SCREEN_SYNCHRONIZATION).setOnPreferenceClickListener(preference -> { ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_synchronization); return true; }); - findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_storage); + findPreference(PREF_SCREEN_IMPORT_EXPORT).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_import_export); return true; }); findPreference(PREF_NOTIFICATION).setOnPreferenceClickListener(preference -> { @@ -130,15 +128,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { startActivity(new Intent(getActivity(), BugReportActivity.class)); return true; }); - findPreference(PREF_STATISTICS).setOnPreferenceClickListener( - preference -> { - new MaterialAlertDialogBuilder(getContext()) - .setMessage(R.string.statistics_moved) - .setPositiveButton(android.R.string.ok, null) - .show(); - return true; - } - ); } private String getLocalizedWebsiteLink() { @@ -166,15 +155,12 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)); config.index(R.xml.preferences_playback) .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_playback)); - config.index(R.xml.preferences_network) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network)); - config.index(R.xml.preferences_storage) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_storage)); + config.index(R.xml.preferences_downloads) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)); config.index(R.xml.preferences_import_export) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_storage)) .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_import_export)); config.index(R.xml.preferences_autodownload) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network)) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)) .addBreadcrumb(R.string.automation) .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload)); config.index(R.xml.preferences_synchronization) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java deleted file mode 100644 index c1cb8adb1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java +++ /dev/null @@ -1,145 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.Settings; -import android.text.format.DateFormat; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.dialog.FeedRefreshIntervalDialog; -import de.danoeh.antennapod.dialog.ProxyDialog; - -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.concurrent.TimeUnit; - -import static android.content.Context.POWER_SERVICE; - - -public class NetworkPreferencesFragment extends PreferenceFragmentCompat - implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; - private static final String PREF_BATTERY_OPTIMIZATION = "prefBatteryOptimization"; - private static final String PREF_PROXY = "prefProxy"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_network); - setupNetworkScreen(); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.network_pref); - PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onStop() { - super.onStop(); - PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onResume() { - super.onResume(); - setUpdateIntervalText(); - setParallelDownloadsText(UserPreferences.getParallelDownloads()); - } - - private void setupNetworkScreen() { - findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); - return true; - }); - findPreference(UserPreferences.PREF_UPDATE_INTERVAL) - .setOnPreferenceClickListener(preference -> { - new FeedRefreshIntervalDialog(getContext()).show(); - return true; - }); - if (Build.VERSION.SDK_INT >= 31) { - PowerManager powerManager = (PowerManager) getContext().getSystemService(POWER_SERVICE); - if (!powerManager.isIgnoringBatteryOptimizations(getContext().getPackageName())) { - findPreference(PREF_BATTERY_OPTIMIZATION).setVisible(true); - findPreference(PREF_BATTERY_OPTIMIZATION).setOnPreferenceClickListener(preference -> { - findPreference(PREF_BATTERY_OPTIMIZATION).setVisible(false); - Intent i = new Intent(); - i.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - i.setData(Uri.parse("package:" + getContext().getPackageName())); - startActivity(i); - return true; - }); - } - } - - findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) - .setOnPreferenceChangeListener( - (preference, o) -> { - if (o instanceof Integer) { - setParallelDownloadsText((Integer) o); - } - return true; - } - ); - // validate and set correct value: number of downloads between 1 and 50 (inclusive) - findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { - ProxyDialog dialog = new ProxyDialog(getActivity()); - dialog.show(); - return true; - }); - } - - /** - * Used to init and handle changes to view - */ - private void setUpdateIntervalText() { - Context context = getActivity().getApplicationContext(); - String val; - long interval = UserPreferences.getUpdateInterval(); - if (interval > 0) { - int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); - val = context.getResources().getQuantityString( - R.plurals.feed_refresh_every_x_hours, hours, hours); - } else { - int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - if (timeOfDay.length == 2) { - Calendar cal = new GregorianCalendar(); - cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); - cal.set(Calendar.MINUTE, timeOfDay[1]); - String timeOfDayStr = DateFormat.getTimeFormat(context).format(cal.getTime()); - val = String.format(context.getString(R.string.feed_refresh_interval_at), - timeOfDayStr); - } else { - val = context.getString(R.string.feed_refresh_never); - } - } - String summary = context.getString(R.string.feed_refresh_sum) + "\n" - + String.format(context.getString(R.string.pref_current_value), val); - findPreference(UserPreferences.PREF_UPDATE_INTERVAL).setSummary(summary); - } - - private void setParallelDownloadsText(int downloads) { - final Resources res = getActivity().getResources(); - String s = res.getString(R.string.parallel_downloads, downloads); - findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { - setUpdateIntervalText(); - } - } -} - - diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java deleted file mode 100644 index 1801b107f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.os.Bundle; -import androidx.preference.PreferenceFragmentCompat; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.dialog.ChooseDataFolderDialog; - -import java.io.File; - -public class StoragePreferencesFragment extends PreferenceFragmentCompat { - private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; - private static final String PREF_IMPORT_EXPORT = "prefImportExport"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_storage); - setupStorageScreen(); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.storage_pref); - } - - @Override - public void onResume() { - super.onResume(); - setDataFolderText(); - } - - private void setupStorageScreen() { - findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( - preference -> { - ChooseDataFolderDialog.showDialog(getContext(), path -> { - UserPreferences.setDataFolder(path); - setDataFolderText(); - }); - return true; - } - ); - findPreference(PREF_IMPORT_EXPORT).setOnPreferenceClickListener( - preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_import_export); - return true; - } - ); - } - - private void setDataFolderText() { - File f = UserPreferences.getDataFolder(null); - if (f != null) { - findPreference(PREF_CHOOSE_DATA_DIR).setSummary(f.getAbsolutePath()); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index caf555964..66f592af2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -5,18 +5,19 @@ import android.os.Build; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AlertDialog; -import com.google.android.material.snackbar.Snackbar; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.core.app.ActivityCompat; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.dialog.DrawerPreferencesDialog; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.greenrobot.eventbus.EventBus; import java.util.List; @@ -37,22 +38,16 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { } private void setupInterfaceScreen() { - findPreference(UserPreferences.PREF_THEME) - .setOnPreferenceChangeListener( - (preference, newValue) -> { - ActivityCompat.recreate(getActivity()); - return true; - }); - + Preference.OnPreferenceChangeListener restartApp = (preference, newValue) -> { + ActivityCompat.recreate(getActivity()); + return true; + }; + findPreference(UserPreferences.PREF_THEME).setOnPreferenceChangeListener(restartApp); + findPreference(UserPreferences.PREF_THEME_BLACK).setOnPreferenceChangeListener(restartApp); + findPreference(UserPreferences.PREF_TINTED_COLORS).setOnPreferenceChangeListener(restartApp); if (Build.VERSION.SDK_INT < 31) { findPreference(UserPreferences.PREF_TINTED_COLORS).setVisible(false); } - findPreference(UserPreferences.PREF_TINTED_COLORS) - .setOnPreferenceChangeListener( - (preference, newValue) -> { - ActivityCompat.recreate(getActivity()); - return true; - }); findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) .setOnPreferenceChangeListener( diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index de6056063..9b9525133 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -10,10 +10,25 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.IntraFeedSortDialog; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.SortOrder; import org.apache.commons.lang3.StringUtils; +import android.content.DialogInterface; +import android.annotation.SuppressLint; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.dialog.RemoveFeedDialog; +import de.danoeh.antennapod.dialog.RenameItemDialog; +import de.danoeh.antennapod.dialog.TagSettingsDialog; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** * Handles interactions with the FeedItemMenu. @@ -51,7 +66,16 @@ public class FeedMenuHandler { if (itemId == R.id.refresh_item) { DBTasks.forceRefreshFeed(context, selectedFeed, true); } else if (itemId == R.id.refresh_complete_item) { - DBTasks.forceRefreshCompleteFeed(context, selectedFeed); + new Thread(() -> { + selectedFeed.setNextPageLink(selectedFeed.getDownload_url()); + selectedFeed.setPageNr(0); + try { + DBWriter.resetPagedFeedPage(selectedFeed).get(); + FeedUpdateManager.runOnce(context, selectedFeed); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); } else if (itemId == R.id.sort_items) { showSortDialog(context, selectedFeed); } else if (itemId == R.id.visit_website_item) { @@ -75,4 +99,38 @@ public class FeedMenuHandler { sortDialog.openDialog(); } + public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, + @NonNull Feed selectedFeed, Runnable callback) { + @NonNull Context context = fragment.requireContext(); + if (menuItemId == R.id.rename_folder_item) { + new RenameItemDialog(fragment.getActivity(), selectedFeed).show(); + } else if (menuItemId == R.id.remove_all_inbox_item) { + ConfirmationDialog dialog = new ConfirmationDialog(fragment.getActivity(), + R.string.remove_all_inbox_label, R.string.remove_all_inbox_confirmation_msg) { + @Override + @SuppressLint("CheckResult") + public void onConfirmButtonPressed(DialogInterface clickedDialog) { + clickedDialog.dismiss(); + Observable.fromCallable((Callable<Future>) () -> DBWriter.removeFeedNewFlag(selectedFeed.getId())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> callback.run(), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + }; + dialog.createNewDialog().show(); + + } else if (menuItemId == R.id.edit_tags) { + TagSettingsDialog.newInstance(Collections.singletonList(selectedFeed.getPreferences())) + .show(fragment.getChildFragmentManager(), TagSettingsDialog.TAG); + } else if (menuItemId == R.id.rename_item) { + new RenameItemDialog(fragment.getActivity(), selectedFeed).show(); + } else if (menuItemId == R.id.remove_feed) { + RemoveFeedDialog.show(context, selectedFeed, null); + } else { + return false; + } + return true; + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index f6bb286f9..0a1b56e25 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -13,7 +13,7 @@ import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.error.CrashReportWriter; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.swipeactions.SwipeAction; import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; @@ -31,7 +31,7 @@ public class PreferenceUpgrader { int newVersion = BuildConfig.VERSION_CODE; if (oldVersion != newVersion) { - AutoUpdateManager.restartUpdateAlarm(context); + FeedUpdateManager.restartUpdateAlarm(context, true); CrashReportWriter.getFile().delete(); upgrade(oldVersion, context); @@ -57,9 +57,6 @@ public class PreferenceUpgrader { } } if (oldVersion < 1070300) { - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, - UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { UserPreferences.setAllowMobileAutoDownload(true); } @@ -138,5 +135,17 @@ public class PreferenceUpgrader { prefs.edit().putBoolean(UserPreferences.PREF_BACK_OPENS_DRAWER, true).apply(); } } + if (oldVersion < 3010000) { + if (prefs.getString(UserPreferences.PREF_THEME, "system").equals("2")) { + prefs.edit() + .putString(UserPreferences.PREF_THEME, "1") + .putBoolean(UserPreferences.PREF_THEME_BLACK, true) + .apply(); + } + UserPreferences.setAllowMobileSync(true); + if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":").contains(":")) { // Unset or "time of day" + prefs.edit().putString(UserPreferences.PREF_UPDATE_INTERVAL, "12").apply(); + } + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/ThemePreference.java b/app/src/main/java/de/danoeh/antennapod/preferences/ThemePreference.java new file mode 100644 index 000000000..30cbeb523 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/preferences/ThemePreference.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.preferences; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.cardview.widget.CardView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; +import com.google.android.material.elevation.SurfaceColors; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.ThemePreferenceBinding; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +public class ThemePreference extends Preference { + ThemePreferenceBinding viewBinding; + + public ThemePreference(Context context) { + super(context); + setLayoutResource(R.layout.theme_preference); + } + + public ThemePreference(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutResource(R.layout.theme_preference); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + viewBinding = ThemePreferenceBinding.bind(holder.itemView); + updateUi(); + } + + void updateThemeCard(CardView card, UserPreferences.ThemePreference theme) { + float density = getContext().getResources().getDisplayMetrics().density; + int surfaceColor = SurfaceColors.getColorForElevation(getContext(), 1 * density); + int surfaceColorActive = SurfaceColors.getColorForElevation(getContext(), 32 * density); + UserPreferences.ThemePreference activeTheme = UserPreferences.getTheme(); + card.setCardBackgroundColor(theme == activeTheme ? surfaceColorActive : surfaceColor); + card.setOnClickListener(v -> { + UserPreferences.setTheme(theme); + if (getOnPreferenceChangeListener() != null) { + getOnPreferenceChangeListener().onPreferenceChange(this, UserPreferences.getTheme()); + } + updateUi(); + }); + } + + void updateUi() { + updateThemeCard(viewBinding.themeSystemCard, UserPreferences.ThemePreference.SYSTEM); + updateThemeCard(viewBinding.themeLightCard, UserPreferences.ThemePreference.LIGHT); + updateThemeCard(viewBinding.themeDarkCard, UserPreferences.ThemePreference.DARK); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java index c566a1fd2..788359a4e 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -8,11 +8,12 @@ import android.util.Log; import android.widget.Toast; import java.util.Arrays; +import java.util.Collections; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.model.feed.Feed; /** @@ -43,9 +44,11 @@ public class SPAReceiver extends BroadcastReceiver{ Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); ClientConfigurator.initialize(context); for (String url : feedUrls) { - Feed f = new Feed(url, null); - DownloadServiceInterface.get().download(context, false, DownloadRequestCreator.create(f).build()); + Feed feed = new Feed(url, null, "Unknown podcast"); + feed.setItems(Collections.emptyList()); + DBTasks.updateFeed(context, feed, false); } Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); + FeedUpdateManager.runOnce(context); } } diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java index da3fd7b05..1a700a296 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java @@ -1,7 +1,10 @@ package de.danoeh.antennapod.ui.home; +import android.Manifest; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -11,20 +14,32 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.databinding.HomeFragmentBinding; import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.fragment.SearchFragment; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.home.sections.AllowNotificationsSection; import de.danoeh.antennapod.ui.home.sections.DownloadsSection; import de.danoeh.antennapod.ui.home.sections.EpisodesSurpriseSection; import de.danoeh.antennapod.ui.home.sections.InboxSection; @@ -35,13 +50,6 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; /** * Shows unread or recently published episodes @@ -69,13 +77,12 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis } viewBinding.homeScrollView.setOnScrollChangeListener(new LiftOnScrollListener(viewBinding.appbar)); ((MainActivity) requireActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); - refreshToolbarState(); populateSectionList(); updateWelcomeScreenVisibility(); viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); viewBinding.swipeRefresh.setOnRefreshListener(() -> { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> viewBinding.swipeRefresh.setRefreshing(false), getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); }); @@ -86,6 +93,11 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis private void populateSectionList() { viewBinding.homeContainer.removeAllViews(); + if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + addSection(new AllowNotificationsSection()); + } + List<String> hiddenSections = getHiddenSections(getContext()); String[] sectionTags = getResources().getStringArray(R.array.home_section_tags); for (String sectionTag : sectionTags) { @@ -126,14 +138,10 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenSectionsString, ","))); } - private void refreshToolbarState() { - MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(), - R.id.refresh_item, DownloadService.isRunning && DownloadService.isDownloadingFeeds()); - } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - refreshToolbarState(); + public void onEventMainThread(FeedUpdateRunningEvent event) { + MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(), + R.id.refresh_item, event.isFeedUpdateRunning); } @Override @@ -142,7 +150,7 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList()); return true; } else if (item.getItemId() == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); + FeedUpdateManager.runOnceOrAsk(requireContext()); return true; } else if (item.getItemId() == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); @@ -178,7 +186,8 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis if (disposable != null) { disposable.dispose(); } - disposable = Observable.fromCallable(() -> DBReader.getNavDrawerData().items.size()) + disposable = Observable.fromCallable(() -> + DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()).items.size()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(numSubscriptions -> { diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java index 7b7d999ad..6cde2927b 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java @@ -14,9 +14,12 @@ import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.DefaultItemAnimator; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; +import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; import de.danoeh.antennapod.databinding.HomeSectionBinding; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import org.greenrobot.eventbus.EventBus; @@ -58,6 +61,12 @@ public abstract class HomeSection extends Fragment implements View.OnCreateConte // Apparently, none of the visibility check method works reliably on its own, so we just use all. return false; } + if (viewBinding.recyclerView.getAdapter() instanceof HorizontalFeedListAdapter) { + HorizontalFeedListAdapter adapter = (HorizontalFeedListAdapter) viewBinding.recyclerView.getAdapter(); + Feed selectedFeed = adapter.getLongPressedItem(); + return selectedFeed != null + && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeed, () -> { }); + } FeedItem longPressedItem; if (viewBinding.recyclerView.getAdapter() instanceof EpisodeItemListAdapter) { EpisodeItemListAdapter adapter = (EpisodeItemListAdapter) viewBinding.recyclerView.getAdapter(); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java new file mode 100644 index 000000000..b023a19bf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.Manifest; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.databinding.HomeSectionNotificationBinding; +import de.danoeh.antennapod.ui.home.HomeFragment; + +public class AllowNotificationsSection extends Fragment { + HomeSectionNotificationBinding viewBinding; + + private final ActivityResultLauncher<String> requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); + } else { + viewBinding.openSettingsButton.setVisibility(View.VISIBLE); + Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); + } + }); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = HomeSectionNotificationBinding.inflate(inflater); + viewBinding.allowButton.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + }); + viewBinding.openSettingsButton.setOnClickListener(view -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }); + return viewBinding.getRoot(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java index 78c336335..1240546c4 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java @@ -23,6 +23,8 @@ import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.home.HomeSection; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; import io.reactivex.Observable; @@ -121,7 +123,9 @@ public class DownloadsSection extends HomeSection { if (disposable != null) { disposable.dispose(); } - disposable = Observable.fromCallable(DBReader::getDownloadedItems) + SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); + disposable = Observable.fromCallable(() -> DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(downloads -> { diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java index e635e9538..8d343a16a 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java @@ -26,7 +26,7 @@ import de.danoeh.antennapod.fragment.InboxFragment; import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.storage.database.PodDBAdapter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.home.HomeSection; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -125,8 +125,9 @@ public class InboxSection extends HomeSection { disposable.dispose(); } disposable = Observable.fromCallable(() -> - new Pair<>(DBReader.getNewItemsList(0, NUM_EPISODES), - PodDBAdapter.getInstance().getNumberOfNewItems())) + new Pair<>(DBReader.getEpisodes(0, NUM_EPISODES, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()), + DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)))) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java index 9d5619845..bafc01bf6 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.fragment.SubscriptionFragment; @@ -44,7 +46,14 @@ public class SubscriptionsSection extends HomeSection { final View view = super.onCreateView(inflater, container, savedInstanceState); viewBinding.recyclerView.setLayoutManager( new LinearLayoutManager(getActivity(), RecyclerView.HORIZONTAL, false)); - listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity()); + listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + super.onCreateContextMenu(contextMenu, view, contextMenuInfo); + MenuItemUtils.setOnClickListeners(contextMenu, SubscriptionsSection.this::onContextItemSelected); + } + }; listAdapter.setDummyViews(NUM_FEEDS); viewBinding.recyclerView.setAdapter(listAdapter); int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); diff --git a/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java b/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java new file mode 100644 index 000000000..3f921b445 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +public class NoRelayoutTextView extends AppCompatTextView { + public NoRelayoutTextView(@NonNull Context context) { + super(context); + } + + public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @SuppressLint("MissingSuperCall") + @Override + public void requestLayout() { + // Deliberate no-op + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java b/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java deleted file mode 100644 index 191f72d2e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; - -/** - * Samsung's Android 6.0.1 has a bug that crashes the app when inflating a time picker. - * This class serves as a workaround for affected devices. - */ -public class TimePicker extends android.widget.TimePicker { - public TimePicker(Context context) { - super(context); - } - - public TimePicker(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - try { - super.onRtlPropertiesChanged(layoutDirection); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index cdf688502..fd3ec9299 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -226,6 +226,10 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } private void updateDuration(PlaybackPositionEvent event) { + if (getFeedItem().getMedia() != null) { + getFeedItem().getMedia().setPosition(event.getPosition()); + getFeedItem().getMedia().setDuration(event.getDuration()); + } int currentPosition = event.getPosition(); int timeDuration = event.getDuration(); int remainingTime = Math.max(timeDuration - currentPosition, 0); diff --git a/app/src/main/play/listings/en-US/full-description.txt b/app/src/main/play/listings/en-US/full-description.txt index 568dd57bf..bffc60cff 100644 --- a/app/src/main/play/listings/en-US/full-description.txt +++ b/app/src/main/play/listings/en-US/full-description.txt @@ -1,4 +1,4 @@ -AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the iTunes podcast database, OPML files or simple RSS URLs. +AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the Apple Podcasts database, OPML files or simple RSS URLs. Download, stream or queue episodes and enjoy them the way you like with adjustable playback speeds, chapter support and a sleep timer. Save effort, battery power and mobile data usage with powerful automation controls for downloading episodes (specify times, intervals and WiFi networks) and deleting episodes (based on your favourites and delay settings). @@ -6,7 +6,7 @@ Made by podcast-enthusiasts, AntennaPod is free in all senses of the word: open <b>Import, organize and play</b> • Manage playback from anywhere: homescreen widget, system notification and earplug and bluetooth controls -• Add and import feeds via the iTunes and gPodder.net directories, OPML files and RSS or Atom links +• Add and import feeds via the Apple Podcasts, gPodder.net, fyyd or Podcast Index directories, OPML files and RSS or Atom links • Enjoy listening your way with adjustable playback speed, chapter support, remembered playback position and an advanced sleep timer (shake to reset, lower volume) • Access password-protected feeds and episodes diff --git a/app/src/main/res/drawable-nodpi/theme_preview_dark.png b/app/src/main/res/drawable-nodpi/theme_preview_dark.png Binary files differnew file mode 100644 index 000000000..b4e1e0376 --- /dev/null +++ b/app/src/main/res/drawable-nodpi/theme_preview_dark.png diff --git a/app/src/main/res/drawable-nodpi/theme_preview_light.png b/app/src/main/res/drawable-nodpi/theme_preview_light.png Binary files differnew file mode 100644 index 000000000..39ef47b4f --- /dev/null +++ b/app/src/main/res/drawable-nodpi/theme_preview_light.png diff --git a/app/src/main/res/drawable-nodpi/theme_preview_system.png b/app/src/main/res/drawable-nodpi/theme_preview_system.png Binary files differnew file mode 100644 index 000000000..cc6403a98 --- /dev/null +++ b/app/src/main/res/drawable-nodpi/theme_preview_system.png diff --git a/app/src/main/res/layout/audio_controls.xml b/app/src/main/res/layout/audio_controls.xml index dc48006bb..3abb70961 100644 --- a/app/src/main/res/layout/audio_controls.xml +++ b/app/src/main/res/layout/audio_controls.xml @@ -32,12 +32,6 @@ android:layout_height="wrap_content" android:text="@string/pref_skip_silence_title" /> - <CheckBox - android:id="@+id/stereo_to_mono" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/stereo_to_mono" /> - </LinearLayout> </ScrollView> diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index 496875a5f..87225e783 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -104,7 +104,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp"> - <TextView + <de.danoeh.antennapod.view.NoRelayoutTextView android:id="@+id/txtvPosition" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -116,7 +116,7 @@ android:textColor="?android:attr/textColorSecondary" android:textSize="@dimen/text_size_micro" /> - <TextView + <de.danoeh.antennapod.view.NoRelayoutTextView android:id="@+id/txtvLength" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -124,6 +124,7 @@ android:layout_alignParentRight="true" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" + android:textAlignment="textEnd" android:background="?android:attr/selectableItemBackground" android:text="@string/position_default_label" android:textColor="?android:attr/textColorSecondary" diff --git a/app/src/main/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml index d6664b56c..19f149f2c 100644 --- a/app/src/main/res/layout/cover_fragment.xml +++ b/app/src/main/res/layout/cover_fragment.xml @@ -90,6 +90,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="8dp" + android:paddingHorizontal="8dp" android:background="@drawable/grey_border" android:clickable="true" android:focusable="true" diff --git a/app/src/main/res/layout/edit_tags_dialog.xml b/app/src/main/res/layout/edit_tags_dialog.xml index 7ad938777..b20facbf0 100644 --- a/app/src/main/res/layout/edit_tags_dialog.xml +++ b/app/src/main/res/layout/edit_tags_dialog.xml @@ -7,6 +7,12 @@ android:orientation="vertical" android:padding="16dp"> + <CheckBox + android:id="@+id/rootFolderCheckbox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/feed_folders_include_root" /> + <com.joanzapata.iconify.widget.IconTextView android:id="@+id/commonTagsInfo" android:layout_width="match_parent" @@ -22,12 +28,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - <CheckBox - android:id="@+id/rootFolderCheckbox" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/feed_folders_include_root" /> - <com.google.android.material.textfield.TextInputLayout android:id="@+id/newTagTextInput" android:layout_width="match_parent" diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml index 4969215d9..4012595bc 100644 --- a/app/src/main/res/layout/external_player_fragment.xml +++ b/app/src/main/res/layout/external_player_fragment.xml @@ -19,7 +19,6 @@ android:id="@+id/imgvCover" android:layout_width="wrap_content" android:layout_height="match_parent" - android:contentDescription="@string/media_player" android:adjustViewBounds="true" android:cropToPadding="true" android:maxWidth="96dp" diff --git a/app/src/main/res/layout/feed_refresh_dialog.xml b/app/src/main/res/layout/feed_refresh_dialog.xml deleted file mode 100644 index 5a6770a80..000000000 --- a/app/src/main/res/layout/feed_refresh_dialog.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<RadioGroup - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/radioGroup" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="16dp"> - - <RadioButton - android:id="@+id/intervalRadioButton" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/feed_refresh_interval" /> - - <Spinner - android:id="@+id/spinner" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:visibility="gone" /> - - <RadioButton - android:id="@+id/timeRadioButton" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/feed_refresh_time" /> - - <de.danoeh.antennapod.view.TimePicker - android:id="@+id/timePicker" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:timePickerMode="spinner" - android:visibility="gone" /> - - <RadioButton - android:id="@+id/disableRadioButton" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/feed_refresh_never" /> - -</RadioGroup> diff --git a/app/src/main/res/layout/home_section_notification.xml b/app/src/main/res/layout/home_section_notification.xml new file mode 100644 index 000000000..7e2182176 --- /dev/null +++ b/app/src/main/res/layout/home_section_notification.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:padding="16dp"> + + <com.google.android.material.card.MaterialCardView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical" + app:cardCornerRadius="12dp" + app:cardElevation="0dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:text="@string/notification_permission_text" /> + + <Button + android:id="@+id/allowButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:text="@android:string/ok" /> + + <Button + android:id="@+id/openSettingsButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:visibility="gone" + android:text="@string/open_settings" /> + + </LinearLayout> + + </com.google.android.material.card.MaterialCardView> + +</LinearLayout> diff --git a/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml index 572096911..48d69e4a9 100644 --- a/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml +++ b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml @@ -10,7 +10,7 @@ android:id="@+id/useGlobalCheckbox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/feed_auto_download_global" + android:text="@string/global_default" android:layout_marginBottom="8dp" /> <LinearLayout diff --git a/app/src/main/res/layout/scrollable_dialog.xml b/app/src/main/res/layout/scrollable_dialog.xml deleted file mode 100644 index 29b84ee4b..000000000 --- a/app/src/main/res/layout/scrollable_dialog.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <ScrollView - android:id="@+id/content" - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" /> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="end" - android:orientation="horizontal" - android:paddingHorizontal="32dp" - android:paddingVertical="16dp" - style="?android:attr/buttonBarStyle"> - - <Button - android:id="@+id/negativeButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" /> - - <Button - android:id="@+id/positiveButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" /> - - </LinearLayout> - -</LinearLayout> diff --git a/app/src/main/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index 50cb9745f..0b689fa28 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -1,64 +1,107 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:foreground="?attr/selectableItemBackground"> + android:padding="4dp"> - <de.danoeh.antennapod.ui.common.SquareImageView - android:id="@+id/imgvCover" + <androidx.cardview.widget.CardView + android:id="@+id/outerContainer" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/non_square_icon_background" - android:scaleType="fitCenter" - squareImageView:direction="width" - tools:src="@tools:sample/avatars" /> + android:clickable="false" + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="12dp" + app:cardElevation="0dp"> - <com.joanzapata.iconify.widget.IconTextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignStart="@+id/imgvCover" - android:layout_alignLeft="@+id/imgvCover" - android:layout_alignTop="@+id/imgvCover" - android:layout_alignEnd="@+id/imgvCover" - android:layout_alignRight="@+id/imgvCover" - android:layout_alignBottom="@+id/imgvCover" - android:background="@color/non_square_icon_background" - android:ellipsize="end" - android:gravity="center" - android:textColor="?android:attr/textColorPrimary" - tools:text="@string/app_name" /> - - <de.danoeh.antennapod.ui.common.TriangleLabelView - android:id="@+id/triangleCountView" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignParentTop="true" - android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" - app:backgroundColor="?attr/colorSecondary" - app:corner="rightTop" - app:primaryText="Test" - app:primaryTextColor="?attr/colorOnSecondary" - app:primaryTextSize="12sp" /> - - <FrameLayout - android:id="@+id/selectView" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <CheckBox - android:id="@+id/selectCheckBox" + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.cardview.widget.CardView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="1px" + android:clickable="false" + app:cardBackgroundColor="@color/non_square_icon_background" + app:cardCornerRadius="12dp" + app:cardElevation="0dp"> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <de.danoeh.antennapod.ui.common.SquareImageView + android:id="@+id/coverImage" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="fitCenter" + android:outlineProvider="background" + squareImageView:direction="width" + tools:src="@tools:sample/avatars" /> + + <TextView + android:id="@+id/fallbackTitleLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignStart="@+id/coverImage" + android:layout_alignLeft="@+id/coverImage" + android:layout_alignTop="@+id/coverImage" + android:layout_alignEnd="@+id/coverImage" + android:layout_alignRight="@+id/coverImage" + android:layout_alignBottom="@+id/coverImage" + android:background="@color/feed_text_bg" + android:gravity="center" + android:ellipsize="end" + android:padding="6dp" + android:textColor="#fff" + tools:text="@sample/episodes.json/data/title" /> + + <TextView + android:id="@+id/countViewPill" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="3" + android:layout_alignParentEnd="true" + android:textSize="14sp" + style="@style/TextPill" /> + + </RelativeLayout> + + </androidx.cardview.widget.CardView> + + <TextView + android:id="@+id/titleLabel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:gravity="start" + android:textColor="?android:attr/textColorPrimary" + android:lines="2" + tools:text="@sample/episodes.json/data/title" /> + + </LinearLayout> + + <FrameLayout + android:id="@+id/selectContainer" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minWidth="0dp" - android:minHeight="0dp" - android:layout_margin="8dp" /> + android:clickable="false"> + + <CheckBox + android:id="@+id/selectCheckBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="0dp" + android:minHeight="0dp" + android:layout_margin="8dp" /> + + </FrameLayout> - </FrameLayout> + </androidx.cardview.widget.CardView> -</RelativeLayout> +</FrameLayout> diff --git a/app/src/main/res/layout/theme_preference.xml b/app/src/main/res/layout/theme_preference.xml new file mode 100644 index 000000000..32a7ed1e8 --- /dev/null +++ b/app/src/main/res/layout/theme_preference.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:background="?android:attr/windowBackground" + android:gravity="top" + android:padding="8dp"> + + <androidx.cardview.widget.CardView + android:id="@+id/themeSystemCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:clickable="true" + android:foreground="?android:attr/selectableItemBackground" + android:layout_weight="1" + app:cardElevation="0dp" + app:cardCornerRadius="16dp" + app:contentPadding="16dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center"> + + <ImageView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:src="@drawable/theme_preview_system" /> + + <TextView + android:id="@+id/themeSystemRadio" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/pref_theme_title_automatic" + android:clickable="false" /> + + </LinearLayout> + + </androidx.cardview.widget.CardView> + + <androidx.cardview.widget.CardView + android:id="@+id/themeLightCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:clickable="true" + android:foreground="?android:attr/selectableItemBackground" + android:layout_weight="1" + app:cardElevation="0dp" + app:cardCornerRadius="16dp" + app:contentPadding="16dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center"> + + <ImageView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:src="@drawable/theme_preview_light" /> + + <TextView + android:id="@+id/themeLightRadio" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/pref_theme_title_light" + android:clickable="false" /> + + </LinearLayout> + + </androidx.cardview.widget.CardView> + + <androidx.cardview.widget.CardView + android:id="@+id/themeDarkCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:clickable="true" + android:foreground="?android:attr/selectableItemBackground" + android:layout_weight="1" + app:cardElevation="0dp" + app:cardCornerRadius="16dp" + app:contentPadding="16dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center"> + + <ImageView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:src="@drawable/theme_preview_dark" /> + + <TextView + android:id="@+id/themeDarkCardRadio" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/pref_theme_title_dark" + android:clickable="false" /> + + </LinearLayout> + + </androidx.cardview.widget.CardView> + +</LinearLayout> diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml index 50001bf9c..1a8f02c7c 100644 --- a/app/src/main/res/layout/time_dialog.xml +++ b/app/src/main/res/layout/time_dialog.xml @@ -141,6 +141,14 @@ android:layout_height="wrap_content" android:text="@string/auto_enable_label" /> + <Button + android:id="@+id/changeTimes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/auto_enable_change_times" + android:layout_gravity="center" + style="@style/Widget.MaterialComponents.Button.TextButton" /> + </LinearLayout> </LinearLayout> diff --git a/app/src/main/res/menu/downloads_completed.xml b/app/src/main/res/menu/downloads_completed.xml index 61a0e6578..08179de58 100644 --- a/app/src/main/res/menu/downloads_completed.xml +++ b/app/src/main/res/menu/downloads_completed.xml @@ -22,5 +22,10 @@ android:menuCategory="container" android:icon="@drawable/ic_refresh" app:showAsAction="always" /> + <item + android:id="@+id/downloads_sort" + android:title="@string/sort"> + <menu></menu> + </item> </menu> diff --git a/app/src/main/res/menu/episodes.xml b/app/src/main/res/menu/episodes.xml index 2841fc12f..358573c93 100644 --- a/app/src/main/res/menu/episodes.xml +++ b/app/src/main/res/menu/episodes.xml @@ -21,7 +21,7 @@ android:icon="@drawable/ic_filter" android:menuCategory="container" android:title="@string/filter" - custom:showAsAction="always"/> + custom:showAsAction="ifRoom"/> <item android:id="@+id/action_favorites" @@ -30,4 +30,11 @@ android:title="@string/favorite_episodes_label" custom:showAsAction="always"/> + <item + android:id="@+id/episodes_sort" + android:title="@string/sort" + custom:showAsAction="never"> + <menu /> + </item> + </menu> diff --git a/app/src/main/res/menu/inbox.xml b/app/src/main/res/menu/inbox.xml index a284a2f56..fba8eefdb 100644 --- a/app/src/main/res/menu/inbox.xml +++ b/app/src/main/res/menu/inbox.xml @@ -16,6 +16,12 @@ android:icon="@drawable/ic_refresh"/> <item + android:id="@+id/inbox_sort" + android:title="@string/sort"> + <menu></menu> + </item> + + <item android:id="@+id/remove_all_inbox_item" android:title="@string/remove_all_inbox_label" android:menuCategory="container" diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml index ebb0befb0..5f60fe6f5 100644 --- a/app/src/main/res/menu/mediaplayer.xml +++ b/app/src/main/res/menu/mediaplayer.xml @@ -66,6 +66,13 @@ </item> <item + android:id="@+id/player_show_chapters" + custom:showAsAction="never" + android:title="@string/chapters_label" + android:visible="false"> + </item> + + <item android:id="@+id/share_item" android:menuCategory="container" custom:showAsAction="never" diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index c1c2d982c..e4bb63808 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -24,88 +24,7 @@ <item android:id="@+id/queue_sort" android:title="@string/sort"> - - <menu> - <item - android:id="@+id/queue_sort_date" - android:title="@string/date"> - - <menu> - <item - android:id="@+id/queue_sort_date_asc" - android:title="@string/sort_old_new"/> - <item - android:id="@+id/queue_sort_date_desc" - android:title="@string/sort_new_old"/> - </menu> - </item> - - <item - android:id="@+id/queue_sort_duration" - android:title="@string/duration"> - - <menu> - <item - android:id="@+id/queue_sort_duration_asc" - android:title="@string/sort_short_long"/> - <item - android:id="@+id/queue_sort_duration_desc" - android:title="@string/sort_long_short"/> - </menu> - </item> - - <item - android:id="@+id/queue_sort_episode_title" - android:title="@string/episode_title"> - - <menu> - <item - android:id="@+id/queue_sort_episode_title_asc" - android:title="@string/sort_a_z"/> - <item - android:id="@+id/queue_sort_episode_title_desc" - android:title="@string/sort_z_a"/> - </menu> - </item> - - <item - android:id="@+id/queue_sort_feed_title" - android:title="@string/feed_title"> - - <menu> - <item - android:id="@+id/queue_sort_feed_title_asc" - android:title="@string/sort_a_z"/> - <item - android:id="@+id/queue_sort_feed_title_desc" - android:title="@string/sort_z_a"/> - </menu> - </item> - - <item - android:id="@+id/queue_sort_random" - android:title="@string/random"> - </item> - - <item - android:id="@+id/queue_sort_smart_shuffle" - android:title="@string/smart_shuffle"> - - <menu> - <item - android:id="@+id/queue_sort_smart_shuffle_asc" - android:title="@string/sort_old_new"/> - <item - android:id="@+id/queue_sort_smart_shuffle_desc" - android:title="@string/sort_new_old"/> - </menu> - </item> - - <item - android:id="@+id/queue_keep_sorted" - android:title="@string/keep_sorted" - android:checkable="true" /> - </menu> + <menu></menu> </item> <item diff --git a/app/src/main/res/menu/sort_menu.xml b/app/src/main/res/menu/sort_menu.xml new file mode 100644 index 000000000..f0a22042b --- /dev/null +++ b/app/src/main/res/menu/sort_menu.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/sort_date" + android:title="@string/date"> + + <menu> + <item + android:id="@+id/sort_date_asc" + android:title="@string/sort_old_new"/> + <item + android:id="@+id/sort_date_desc" + android:title="@string/sort_new_old"/> + </menu> + </item> + + <item + android:id="@+id/sort_duration" + android:title="@string/duration"> + + <menu> + <item + android:id="@+id/sort_duration_asc" + android:title="@string/sort_short_long"/> + <item + android:id="@+id/sort_duration_desc" + android:title="@string/sort_long_short"/> + </menu> + </item> + + <item + android:id="@+id/sort_episode_title" + android:title="@string/episode_title"> + + <menu> + <item + android:id="@+id/sort_episode_title_asc" + android:title="@string/sort_a_z"/> + <item + android:id="@+id/sort_episode_title_desc" + android:title="@string/sort_z_a"/> + </menu> + </item> + + <item + android:id="@+id/sort_feed_title" + android:title="@string/feed_title"> + + <menu> + <item + android:id="@+id/sort_feed_title_asc" + android:title="@string/sort_a_z"/> + <item + android:id="@+id/sort_feed_title_desc" + android:title="@string/sort_z_a"/> + </menu> + </item> + + <item + android:id="@+id/sort_random" + android:title="@string/random"> + </item> + + <item + android:id="@+id/sort_smart_shuffle" + android:title="@string/smart_shuffle"> + + <menu> + <item + android:id="@+id/sort_smart_shuffle_asc" + android:title="@string/sort_old_new"/> + <item + android:id="@+id/sort_smart_shuffle_desc" + android:title="@string/sort_new_old"/> + </menu> + </item> + <item + android:id="@+id/keep_sorted" + android:title="@string/keep_sorted" + android:checkable="true" /> + +</menu> diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index a2ea42f01..619ab6296 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -45,7 +45,7 @@ android:entryValues="@array/spnAutoDeleteValues" android:icon="@drawable/ic_delete" android:key="autoDelete" - android:summary="@string/feed_auto_download_global" + android:summary="@string/global_default" android:title="@string/auto_delete_label" /> <de.danoeh.antennapod.preferences.MaterialListPreference @@ -57,6 +57,14 @@ android:summary="@string/feed_volume_reduction_summary" android:title="@string/feed_volume_reduction" /> + <de.danoeh.antennapod.preferences.MaterialListPreference + android:entries="@array/feedNewEpisodesActionItems" + android:entryValues="@array/feedNewEpisodesActionValues" + android:icon="@drawable/ic_feed" + android:key="feedNewEpisodesAction" + android:summary="@string/global_default" + android:title="@string/pref_new_episodes_action_title" /> + <PreferenceCategory android:key="autoDownloadCategory" android:title="@string/auto_download_settings_label"> diff --git a/app/src/main/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml index 803cc89ed..25f4e274a 100644 --- a/app/src/main/res/xml/player_widget_info.xml +++ b/app/src/main/res/xml/player_widget_info.xml @@ -7,5 +7,6 @@ android:minHeight="40dp" android:minWidth="250dp" android:minResizeWidth="40dp" + android:widgetFeatures="reconfigurable" android:configure="de.danoeh.antennapod.activity.WidgetConfigActivity"> </appwidget-provider> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 38b11cbab..8f3851c09 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -22,10 +22,10 @@ android:icon="@drawable/ic_play_24dp" /> <Preference - android:key="prefScreenNetwork" - android:title="@string/network_pref" - android:summary="@string/network_pref_sum" - android:icon="@drawable/ic_network" /> + android:key="prefScreenDownloads" + android:title="@string/downloads_pref" + android:summary="@string/downloads_pref_sum" + android:icon="@drawable/ic_download" /> <Preference android:key="prefScreenSynchronization" @@ -34,9 +34,9 @@ android:icon="@drawable/ic_cloud" /> <Preference - android:key="prefScreenStorage" - android:title="@string/storage_pref" - android:summary="@string/storage_sum" + android:key="prefScreenImportExport" + android:title="@string/import_export_pref" + android:summary="@string/import_export_summary" android:icon="@drawable/ic_storage" /> <Preference @@ -44,12 +44,6 @@ android:title="@string/notification_pref_fragment" android:icon="@drawable/ic_notifications"/> - <Preference - android:key="statistics" - android:title="@string/statistics_label" - android:summary="@string/statistics_moved" - android:icon="@drawable/ic_chart_box"/> - <PreferenceCategory android:key="project" android:title="@string/project_pref"> diff --git a/app/src/main/res/xml/preferences_downloads.xml b/app/src/main/res/xml/preferences_downloads.xml new file mode 100644 index 000000000..865748d20 --- /dev/null +++ b/app/src/main/res/xml/preferences_downloads.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:numberpicker="http://schemas.android.com/apk/de.danoeh.antennapod" + xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> + + <Preference + android:title="@string/choose_data_directory" + android:key="prefChooseDataDir"/> + + <PreferenceCategory android:title="@string/automation"> + <de.danoeh.antennapod.preferences.MaterialListPreference + android:entryValues="@array/feed_refresh_interval_values" + android:entries="@array/feed_refresh_interval_entries" + android:key="prefAutoUpdateIntervall" + android:title="@string/feed_refresh_title" + android:summary="@string/feed_refresh_sum" + android:defaultValue="12"/> + <de.danoeh.antennapod.preferences.MaterialListPreference + android:entryValues="@array/globalNewEpisodesActionValues" + android:entries="@array/globalNewEpisodesActionItems" + android:key="prefNewEpisodesAction" + android:title="@string/pref_new_episodes_action_title" + android:summary="@string/pref_new_episodes_action_sum" + android:defaultValue="1"/> + <Preference + android:summary="@string/pref_automatic_download_sum" + android:key="prefAutoDownloadSettings" + android:title="@string/pref_automatic_download_title" + search:ignore="true" /> + <SwitchPreferenceCompat + android:defaultValue="false" + android:enabled="true" + android:key="prefAutoDelete" + android:summary="@string/pref_auto_delete_sum" + android:title="@string/pref_auto_delete_title"/> + <SwitchPreferenceCompat + android:defaultValue="true" + android:enabled="true" + android:key="prefFavoriteKeepsEpisode" + android:summary="@string/pref_favorite_keeps_episodes_sum" + android:title="@string/pref_favorite_keeps_episodes_title"/> + <SwitchPreferenceCompat + android:defaultValue="false" + android:enabled="true" + android:key="prefDeleteRemovesFromQueue" + android:summary="@string/pref_delete_removes_from_queue_sum" + android:title="@string/pref_delete_removes_from_queue_title"/> + </PreferenceCategory> + + <PreferenceCategory android:title="@string/download_pref_details"> + <MultiSelectListPreference + android:defaultValue="@array/mobile_update_default_value" + android:entries="@array/mobile_update_entries" + android:entryValues="@array/mobile_update_values" + android:key="prefMobileUpdateTypes" + android:summary="@string/pref_mobileUpdate_sum" + android:title="@string/pref_mobileUpdate_title"/> + <de.danoeh.antennapod.preferences.NumberPickerPreference + android:defaultValue="4" + numberpicker:minValue="1" + numberpicker:maxValue="50" + android:key="prefParallelDownloads" + android:title="@string/pref_parallel_downloads_title"/> + <Preference + android:key="prefProxy" + android:summary="@string/pref_proxy_sum" + android:title="@string/pref_proxy_title"/> + </PreferenceCategory> +</PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_network.xml b/app/src/main/res/xml/preferences_network.xml deleted file mode 100644 index 34832378f..000000000 --- a/app/src/main/res/xml/preferences_network.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:numberpicker="http://schemas.android.com/apk/de.danoeh.antennapod" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> - <PreferenceCategory android:title="@string/automation"> - <Preference - android:key="prefBatteryOptimization" - android:title="@string/battery_optimization_pref_title" - android:summary="@string/battery_optimization_pref" - app:isPreferenceVisible="false" /> - <Preference - android:key="prefAutoUpdateIntervall" - android:summary="@string/feed_refresh_sum" - android:title="@string/feed_refresh_title"/> - <Preference - android:summary="@string/pref_automatic_download_sum" - android:key="prefAutoDownloadSettings" - android:title="@string/pref_automatic_download_title" - search:ignore="true" /> - </PreferenceCategory> - - <PreferenceCategory android:title="@string/download_pref_details"> - <MultiSelectListPreference - android:defaultValue="@array/mobile_update_default_value" - android:entries="@array/mobile_update_entries" - android:entryValues="@array/mobile_update_values" - android:key="prefMobileUpdateTypes" - android:summary="@string/pref_mobileUpdate_sum" - android:title="@string/pref_mobileUpdate_title"/> - <de.danoeh.antennapod.preferences.NumberPickerPreference - android:defaultValue="4" - numberpicker:minValue="1" - numberpicker:maxValue="50" - android:key="prefParallelDownloads" - android:title="@string/pref_parallel_downloads_title"/> - <Preference - android:key="prefProxy" - android:summary="@string/pref_proxy_sum" - android:title="@string/pref_proxy_title"/> - </PreferenceCategory> -</PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index f84ee01df..57bfe21d7 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -105,14 +105,4 @@ android:summary="@string/pref_skip_keeps_episodes_sum" android:title="@string/pref_skip_keeps_episodes_title"/> </PreferenceCategory> - - <PreferenceCategory android:title="@string/experimental_pref"> - <de.danoeh.antennapod.preferences.MaterialListPreference - android:defaultValue="exoplayer" - android:entries="@array/media_player_options" - android:key="prefMediaPlayer" - android:title="@string/media_player" - android:summary="@string/pref_media_player_message" - android:entryValues="@array/media_player_values"/> - </PreferenceCategory> </PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_storage.xml b/app/src/main/res/xml/preferences_storage.xml deleted file mode 100644 index 89e8c4cf5..000000000 --- a/app/src/main/res/xml/preferences_storage.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> - - <Preference - android:title="@string/choose_data_directory" - android:key="prefChooseDataDir"/> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefAutoDelete" - android:summary="@string/pref_auto_delete_sum" - android:title="@string/pref_auto_delete_title"/> - <SwitchPreferenceCompat - android:defaultValue="true" - android:enabled="true" - android:key="prefFavoriteKeepsEpisode" - android:summary="@string/pref_favorite_keeps_episodes_sum" - android:title="@string/pref_favorite_keeps_episodes_title"/> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefDeleteRemovesFromQueue" - android:summary="@string/pref_delete_removes_from_queue_sum" - android:title="@string/pref_delete_removes_from_queue_title"/> - <Preference - android:title="@string/import_export_pref" - android:summary="@string/import_export_summary" - android:key="prefImportExport" - search:ignore="true"/> -</PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_swipe.xml b/app/src/main/res/xml/preferences_swipe.xml index d555e70e6..10ac102dd 100644 --- a/app/src/main/res/xml/preferences_swipe.xml +++ b/app/src/main/res/xml/preferences_swipe.xml @@ -23,6 +23,6 @@ <Preference android:key="prefSwipeFeed" - android:title="@string/feeds_label"/> + android:title="@string/individual_subscription"/> </PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index 66b971089..733649ce1 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -4,13 +4,13 @@ xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> <PreferenceCategory android:title="@string/appearance"> - <de.danoeh.antennapod.preferences.MaterialListPreference - android:entryValues="@array/theme_values" - android:entries="@array/theme_options" - android:title="@string/pref_set_theme_title" - android:key="prefTheme" - android:summary="@string/pref_set_theme_sum" - android:defaultValue="system"/> + <de.danoeh.antennapod.preferences.ThemePreference + android:key="prefTheme" /> + <SwitchPreferenceCompat + android:title="@string/pref_black_theme_title" + android:key="prefThemeBlack" + android:summary="@string/pref_black_theme_message" + android:defaultValue="false" /> <SwitchPreferenceCompat android:title="@string/pref_tinted_theme_title" android:key="prefTintedColors" @@ -73,12 +73,6 @@ android:key="prefCompactNotificationButtons" android:summary="@string/pref_compact_notification_buttons_sum" android:title="@string/pref_compact_notification_buttons_title"/> - <SwitchPreferenceCompat - android:defaultValue="true" - android:enabled="true" - android:key="prefLockscreenBackground" - android:summary="@string/pref_lockscreen_background_sum" - android:title="@string/pref_lockscreen_background_title"/> </PreferenceCategory> <PreferenceCategory android:title="@string/behavior"> <de.danoeh.antennapod.preferences.MaterialListPreference diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 0e047cf5f..404e9d493 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -3,4 +3,5 @@ <external-path name="external_storage" path="." /> <external-files-path name="external_file_storage" path="." /> <files-path name="name" path="." /> + <root-path name="root" path="." /> </paths> diff --git a/build.gradle b/build.gradle index ae624bfdb..1a16e1dac 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.4.0' classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.0" classpath 'org.codehaus.groovy:groovy-xml:3.0.9' } @@ -45,7 +45,6 @@ project.ext { rxJavaVersion = "2.2.2" iconifyVersion = "2.2.2" exoPlayerVersion = "2.14.2" - audioPlayerVersion = "v2.0.0" // Google Play build wearableSupportVersion = "2.6.0" diff --git a/common.gradle b/common.gradle index 258f512f1..e7f55e082 100644 --- a/common.gradle +++ b/common.gradle @@ -1,9 +1,9 @@ android { - compileSdk 31 + compileSdk 33 defaultConfig { minSdk 21 - targetSdk 31 + targetSdk 33 multiDexEnabled true vectorDrawables.useSupportLibrary true @@ -24,16 +24,17 @@ android { } packagingOptions { - exclude "META-INF/LICENSE.txt" - exclude "META-INF/NOTICE.txt" - // Extraneous jsoup files - exclude "META-INF/CHANGES" - exclude "META-INF/README.md" + resources { + excludes += ["META-INF/LICENSE.txt", + "META-INF/NOTICE.txt", + "META-INF/CHANGES", + "META-INF/README.md"] + } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } testOptions { @@ -43,7 +44,7 @@ android { } } - lintOptions { + lint { disable "GradleDependency" checkDependencies true warningsAsErrors true diff --git a/core/build.gradle b/core/build.gradle index 466b4ea74..526589230 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -5,7 +5,7 @@ apply from: "../common.gradle" apply from: "../playFlavor.gradle" android { - lintOptions { + lint { disable "InvalidPeriodicWorkRequestInterval", "ObsoleteLintCustomCheck", "DefaultLocale", "UnusedAttribute", "ParcelClassLoader", "CheckResult", "TrustAllX509TrustManager", "StaticFieldLeak", "IconDensities", "IconDuplicates" @@ -62,11 +62,11 @@ dependencies { annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion" implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" + implementation 'com.annimon:stream:1.2.2' implementation "com.google.android.exoplayer:exoplayer-core:$exoPlayerVersion" implementation "com.google.android.exoplayer:exoplayer-ui:$exoPlayerVersion" implementation "com.google.android.exoplayer:extension-okhttp:$exoPlayerVersion" - implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" // Non-free dependencies: playApi "com.google.android.support:wearable:$wearableSupportVersion" diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java index 373b24bc8..5998a1e2f 100644 --- a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java +++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java @@ -4,8 +4,10 @@ import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; class WearMediaSession { - static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, - CharSequence name, int icon) { + /** + * Take a custom action builder and add no extras, because this is not the Play version of the app. + */ + static void addWearExtrasToAction(PlaybackStateCompat.CustomAction.Builder actionBuilder) { // no-op } diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 3d7b3b473..6f5508f27 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -59,6 +59,7 @@ <action android:name="android.service.quicksettings.action.QS_TILE" /> </intent-filter> <meta-data android:name="android.service.quicksettings.ACTIVE_TILE" android:value="true" /> + <meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" /> </service> </application> diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java index 9dab98939..360c8c9e2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -11,6 +11,4 @@ public class ClientConfig { public static String USER_AGENT; public static ApplicationCallbacks applicationCallbacks; - - public static DownloadServiceCallbacks downloadServiceCallbacks; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java deleted file mode 100644 index 53ed63c2d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.app.PendingIntent; -import android.content.Context; - -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; - -/** - * Callbacks for the DownloadService of the core module. - */ -public interface DownloadServiceCallbacks { - - /** - * Returns a PendingIntent for a notification the main notification of the DownloadService. - * <p/> - * The PendingIntent takes the users to a screen where they can observe all currently running - * downloads. - * - * @return A non-null PendingIntent for the notification. - */ - PendingIntent getNotificationContentIntent(Context context); - - /** - * Returns a PendingIntent for a notification that tells the user to enter a username - * or a password for a requested download. - * <p/> - * The PendingIntent takes users to an Activity that lets the user enter their username - * and password to retry the download. - * - * @return A non-null PendingIntent for the notification. - */ - PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request); - - /** - * Returns a PendingIntent for notification that notifies the user about the completion of downloads - * along with information about failed and successful downloads. - * <p/> - * The PendingIntent takes users to an activity where they can look at all successful and failed downloads. - * - * @return A non-null PendingIntent for the notification - */ - PendingIntent getReportNotificationContentIntent(Context context); - - /** - * Returns a PendingIntent for notification that notifies the user about the episodes that have been automatically - * downloaded. - * <p/> - * The PendingIntent takes users to an activity where they can look at their episode queue. - * - * @return A non-null PendingIntent for the notification - */ - PendingIntent getAutoDownloadReportNotificationContentIntent(Context context); -} - diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 9046b7165..79c6dd6bc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -8,9 +8,8 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import org.apache.commons.io.IOUtils; import org.xmlpull.v1.XmlPullParserException; @@ -30,6 +29,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import de.danoeh.antennapod.core.export.opml.OpmlElement; import de.danoeh.antennapod.core.export.opml.OpmlReader; @@ -144,9 +144,10 @@ public class OpmlBackupAgent extends BackupAgentHelper { mChecksum = digester == null ? null : digester.digest(); for (OpmlElement opmlElem : opmlElements) { Feed feed = new Feed(opmlElem.getXmlUrl(), null, opmlElem.getText()); - DownloadRequest request = DownloadRequestCreator.create(feed).build(); - DownloadServiceInterface.get().download(mContext, false, request); + feed.setItems(Collections.emptyList()); + DBTasks.updateFeed(mContext, feed, false); } + FeedUpdateManager.runOnce(mContext); } catch (XmlPullParserException e) { Log.e(TAG, "Error while parsing the OPML file", e); } catch (IOException e) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java index 092329229..649ec815a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java @@ -18,6 +18,7 @@ import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.model.feed.SortOrder; /** Writes saved favorites to file. */ public class FavoritesWriter implements ExportWriter { @@ -42,8 +43,8 @@ public class FavoritesWriter implements ExportWriter { InputStream feedTemplateStream = context.getAssets().open(FEED_TEMPLATE); String feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8); - List<FeedItem> allFavorites = DBReader.getRecentlyPublishedEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.IS_FAVORITE)); + List<FeedItem> allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD); Map<Long, List<FeedItem>> favoriteByFeed = getFeedMap(allFavorites); writer.append(templateParts[0]); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 8ee924243..d4d948b2a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -77,7 +77,7 @@ public class LocalFeedUpdater { @VisibleForTesting static void tryUpdateFeed(Feed feed, Context context, Uri folderUri, - UpdaterProgressListener updaterProgressListener) { + UpdaterProgressListener updaterProgressListener) throws IOException { if (feed.getItems() == null) { feed.setItems(new ArrayList<>()); } @@ -124,15 +124,14 @@ public class LocalFeedUpdater { feed.setImageUrl(getImageUrl(allFiles, folderUri)); feed.getPreferences().setAutoDownload(false); - feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); + feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NEVER); feed.setDescription(context.getString(R.string.local_feed_description)); feed.setAuthor(context.getString(R.string.local_folder)); - // update items, delete items without existing file; - // only delete items if the folder contains at least one element to avoid accidentally - // deleting played state or position in case the folder is temporarily unavailable. - boolean removeUnlistedItems = (newItems.size() >= 1); - DBTasks.updateFeed(context, feed, removeUnlistedItems); + if (newItems.isEmpty()) { + throw new IOException("Empty folder. Make sure that the folder is accessible and contains media files."); + } + DBTasks.updateFeed(context, feed, true); } /** @@ -200,48 +199,48 @@ public class LocalFeedUpdater { return item; } - private static void loadMetadata(FeedItem item, FastDocumentFile file, Context context) { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(context, file.getUri()); - - String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); - if (!TextUtils.isEmpty(dateStr)) { - try { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()); - item.setPubDate(simpleDateFormat.parse(dateStr)); - } catch (ParseException parseException) { - Date date = DateUtils.parse(dateStr); - if (date != null) { - item.setPubDate(date); + private static void loadMetadata(FeedItem item, FastDocumentFile file, Context context) throws IOException { + try (MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever()) { + mediaMetadataRetriever.setDataSource(context, file.getUri()); + + String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); + if (!TextUtils.isEmpty(dateStr) && !"19040101T000000.000Z".equals(dateStr)) { + try { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()); + item.setPubDate(simpleDateFormat.parse(dateStr)); + } catch (ParseException parseException) { + Date date = DateUtils.parse(dateStr); + if (date != null) { + item.setPubDate(date); + } } } - } - - String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); - if (!TextUtils.isEmpty(title)) { - item.setTitle(title); - } - String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - item.getMedia().setDuration((int) Long.parseLong(durationStr)); + String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + if (!TextUtils.isEmpty(title)) { + item.setTitle(title); + } - item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); - mediaMetadataRetriever.close(); + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + item.getMedia().setDuration((int) Long.parseLong(durationStr)); - try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { - Id3MetadataReader reader = new Id3MetadataReader( - new CountingInputStream(new BufferedInputStream(inputStream))); - reader.readInputStream(); - item.setDescriptionIfLonger(reader.getComment()); - } catch (IOException | ID3ReaderException e) { - Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage()); + item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { - VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream); + Id3MetadataReader reader = new Id3MetadataReader( + new CountingInputStream(new BufferedInputStream(inputStream))); reader.readInputStream(); - item.setDescriptionIfLonger(reader.getDescription()); - } catch (IOException | VorbisCommentReaderException e2) { - Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage()); + item.setDescriptionIfLonger(reader.getComment()); + } catch (IOException | ID3ReaderException e) { + Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage()); + + try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { + VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream); + reader.readInputStream(); + item.setDescriptionIfLonger(reader.getDescription()); + } catch (IOException | VorbisCommentReaderException e2) { + Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage()); + } } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java index b56e7e6f3..96ffe5cb6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java @@ -2,9 +2,10 @@ package de.danoeh.antennapod.core.preferences; import android.content.Context; import android.content.SharedPreferences; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import java.util.concurrent.TimeUnit; public class SleepTimerPreferences { @@ -17,8 +18,12 @@ public class SleepTimerPreferences { private static final String PREF_VIBRATE = "Vibrate"; private static final String PREF_SHAKE_TO_RESET = "ShakeToReset"; private static final String PREF_AUTO_ENABLE = "AutoEnable"; + private static final String PREF_AUTO_ENABLE_FROM = "AutoEnableFrom"; + private static final String PREF_AUTO_ENABLE_TO = "AutoEnableTo"; - private static final String DEFAULT_VALUE = "15"; + private static final String DEFAULT_LAST_TIMER = "15"; + private static final int DEFAULT_AUTO_ENABLE_FROM = 22; + private static final int DEFAULT_AUTO_ENABLE_TO = 6; private static SharedPreferences prefs; @@ -37,7 +42,7 @@ public class SleepTimerPreferences { } public static String lastTimerValue() { - return prefs.getString(PREF_VALUE, DEFAULT_VALUE); + return prefs.getString(PREF_VALUE, DEFAULT_LAST_TIMER); } public static long timerMillis() { @@ -69,4 +74,33 @@ public class SleepTimerPreferences { return prefs.getBoolean(PREF_AUTO_ENABLE, false); } + public static void setAutoEnableFrom(int hourOfDay) { + prefs.edit().putInt(PREF_AUTO_ENABLE_FROM, hourOfDay).apply(); + } + + public static int autoEnableFrom() { + return prefs.getInt(PREF_AUTO_ENABLE_FROM, DEFAULT_AUTO_ENABLE_FROM); + } + + public static void setAutoEnableTo(int hourOfDay) { + prefs.edit().putInt(PREF_AUTO_ENABLE_TO, hourOfDay).apply(); + } + + public static int autoEnableTo() { + return prefs.getInt(PREF_AUTO_ENABLE_TO, DEFAULT_AUTO_ENABLE_TO); + } + + public static boolean isInTimeRange(int from, int to, int current) { + // Range covers one day + if (from < to) { + return from <= current && current < to; + } + + // Range covers two days + if (from <= current) { + return true; + } + + return current < to; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/ThemeSwitcher.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/ThemeSwitcher.java index b7e4934f5..afe814fcb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/ThemeSwitcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/ThemeSwitcher.java @@ -59,12 +59,15 @@ public abstract class ThemeSwitcher { if (theme == UserPreferences.ThemePreference.SYSTEM) { int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (nightMode == Configuration.UI_MODE_NIGHT_YES) { - return UserPreferences.ThemePreference.DARK; + theme = UserPreferences.ThemePreference.DARK; } else { - return UserPreferences.ThemePreference.LIGHT; + theme = UserPreferences.ThemePreference.LIGHT; } } + if (theme == UserPreferences.ThemePreference.DARK && UserPreferences.getIsBlackTheme()) { + theme = UserPreferences.ThemePreference.BLACK; + } return theme; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index 9ce89ebe2..e30b49280 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; /** * Refreshes all feeds when it receives an intent @@ -20,7 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { Log.d(TAG, "Received intent"); ClientConfigurator.initialize(context); - AutoUpdateManager.runOnce(context); + FeedUpdateManager.runOnce(context); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index 49c5211b0..8d9f046e2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -1,47 +1,179 @@ package de.danoeh.antennapod.core.service; +import android.app.Notification; import android.content.Context; -import androidx.annotation.NonNull; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; - +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.service.download.DefaultDownloaderFactory; +import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; +import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -public class FeedUpdateWorker extends Worker { +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +public class FeedUpdateWorker extends Worker { private static final String TAG = "FeedUpdateWorker"; - public static final String PARAM_RUN_ONCE = "runOnce"; + private final NewEpisodesNotification newEpisodesNotification; + private final NotificationManagerCompat notificationManager; public FeedUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); + newEpisodesNotification = new NewEpisodesNotification(); + notificationManager = NotificationManagerCompat.from(context); } @Override @NonNull public Result doWork() { - final boolean isRunOnce = getInputData().getBoolean(PARAM_RUN_ONCE, false); - Log.d(TAG, "doWork() : isRunOnce = " + isRunOnce); ClientConfigurator.initialize(getApplicationContext()); + newEpisodesNotification.loadCountersBeforeRefresh(); - if (NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed()) { - DBTasks.refreshAllFeeds(getApplicationContext(), false); - } else { + if (!NetworkUtils.networkAvailable() || !NetworkUtils.isFeedRefreshAllowed()) { Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); + return Result.retry(); } - if (!isRunOnce && UserPreferences.isAutoUpdateTimeOfDay()) { - // WorkManager does not allow to set specific time for repeated tasks. - // We repeatedly schedule a OneTimeWorkRequest instead. - AutoUpdateManager.restartUpdateAlarm(getApplicationContext()); + List<Feed> toUpdate; + long feedId = getInputData().getLong(FeedUpdateManager.EXTRA_FEED_ID, -1); + if (feedId == -1) { // Update all + toUpdate = DBReader.getFeedList(); + Iterator<Feed> itr = toUpdate.iterator(); + while (itr.hasNext()) { + Feed feed = itr.next(); + if (!feed.getPreferences().getKeepUpdated()) { + itr.remove(); + } + } + Collections.shuffle(toUpdate); // If the worker gets cancelled early, every feed has a chance to be updated + refreshFeeds(toUpdate, false); + } else { + toUpdate = new ArrayList<>(); + Feed feed = DBReader.getFeed(feedId); + if (feed == null) { + return Result.success(); + } + toUpdate.add(feed); + refreshFeeds(toUpdate, true); } - + notificationManager.cancel(R.id.notification_updating_feeds); return Result.success(); } + + @NonNull + private Notification createNotification(List<Feed> toUpdate) { + Context context = getApplicationContext(); + String contentText = context.getResources().getQuantityString(R.plurals.downloads_left, + toUpdate.size(), toUpdate.size()); + String bigText = Stream.of(toUpdate).map(feed -> "• " + feed.getTitle()).collect(Collectors.joining("\n")); + return new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) + .setContentTitle(context.getString(R.string.download_notification_title_feeds)) + .setContentText(contentText) + .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) + .setSmallIcon(R.drawable.ic_notification_sync) + .setOngoing(true) + .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), + WorkManager.getInstance(context).createCancelPendingIntent(getId())) + .build(); + } + + private void refreshFeeds(List<Feed> toUpdate, boolean force) { + while (!toUpdate.isEmpty()) { + if (isStopped()) { + return; + } + notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate)); + Feed feed = toUpdate.get(0); + try { + if (feed.isLocalFeed()) { + LocalFeedUpdater.updateFeed(feed, getApplicationContext(), null); + } else { + refreshFeed(feed, force); + } + } catch (Exception e) { + DBWriter.setFeedLastUpdateFailed(feed.getId(), true); + DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, e.getMessage(), true); + DBWriter.addDownloadStatus(status); + } + toUpdate.remove(0); + } + } + + void refreshFeed(Feed feed, boolean force) throws Exception { + boolean nextPage = getInputData().getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false) + && feed.getNextPageLink() != null; + if (nextPage) { + feed.setPageNr(feed.getPageNr() + 1); + } + DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); + builder.setForce(force || feed.hasLastUpdateFailed()); + if (nextPage) { + builder.setSource(feed.getNextPageLink()); + } + DownloadRequest request = builder.build(); + + Downloader downloader = new DefaultDownloaderFactory().create(request); + if (downloader == null) { + throw new Exception("Unable to create downloader"); + } + + downloader.call(); + + if (!downloader.getResult().isSuccessful()) { + if (downloader.getResult().isCancelled()) { + return; + } + DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); + DBWriter.addDownloadStatus(downloader.getResult()); + return; + } + + FeedSyncTask feedSyncTask = new FeedSyncTask(getApplicationContext(), request); + boolean success = feedSyncTask.run(); + + if (!success) { + DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); + DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); + return; + } + + if (request.getFeedfileId() == 0) { + return; // No download logs for new subscriptions + } + // we create a 'successful' download log if the feed's last refresh failed + List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + if (log.size() > 0 && !log.get(0).isSuccessful()) { + DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); + } + newEpisodesNotification.showIfNeeded(getApplicationContext(), feedSyncTask.getSavedFeed()); + if (downloader.permanentRedirectUrl != null) { + DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl); + } else if (feedSyncTask.getRedirectUrl() != null) { + DBWriter.updateFeedDownloadURL(request.getSource(), feedSyncTask.getRedirectUrl()); + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 30745a60d..9c238137e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -10,18 +10,29 @@ import android.content.IntentFilter; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.app.NotificationManagerCompat; import androidx.core.app.ServiceCompat; - import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; +import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; +import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; +import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.apache.commons.io.FileUtils; import org.greenrobot.eventbus.EventBus; @@ -38,22 +49,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; -import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; -import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; -import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.model.download.DownloadError; - /** * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent. * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUESTS field of @@ -68,7 +63,6 @@ public class DownloadService extends Service { public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAll"; public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; public static final String EXTRA_REQUESTS = "downloadRequests"; - public static final String EXTRA_REFRESH_ALL = "refreshAll"; public static final String EXTRA_INITIATED_BY_USER = "initiatedByUser"; public static final String EXTRA_CLEANUP_MEDIA = "cleanupMedia"; @@ -82,8 +76,8 @@ public class DownloadService extends Service { private final ExecutorService downloadEnqueueExecutor; private final List<DownloadStatus> reportQueue = new ArrayList<>(); + private final List<DownloadRequest> failedRequestsForReport = new ArrayList<>(); private DownloadServiceNotification notificationManager; - private final NewEpisodesNotification newEpisodesNotification; private NotificationUpdater notificationUpdater; private ScheduledFuture<?> notificationUpdaterFuture; private ScheduledFuture<?> downloadPostFuture; @@ -97,16 +91,12 @@ public class DownloadService extends Service { } public DownloadService() { - newEpisodesNotification = new NewEpisodesNotification(); downloadEnqueueExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "EnqueueThread"); t.setPriority(Thread.MIN_PRIORITY); return t; }); - // Must be the first runnable in syncExecutor - downloadEnqueueExecutor.execute(newEpisodesNotification::loadCountersBeforeRefresh); - Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); downloadHandleExecutor = Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), r -> { @@ -138,18 +128,6 @@ public class DownloadService extends Service { connectionMonitor.enable(getApplicationContext()); } - public static boolean isDownloadingFeeds() { - if (!isRunning) { - return false; - } - for (Downloader downloader : downloads) { - if (downloader.request.getFeedfileType() == Feed.FEEDFILETYPE_FEED && !downloader.cancelled) { - return true; - } - } - return false; - } - public static boolean isDownloadingFile(String downloadUrl) { if (!isRunning) { return false; @@ -176,13 +154,10 @@ public class DownloadService extends Service { if (intent != null && intent.hasExtra(EXTRA_REQUESTS)) { Notification notification = notificationManager.updateNotifications(downloads); startForeground(R.id.notification_downloading, notification); + NotificationManagerCompat.from(this).cancel(R.id.notification_download_report); + NotificationManagerCompat.from(this).cancel(R.id.notification_auto_download_report); setupNotificationUpdaterIfNecessary(); downloadEnqueueExecutor.execute(() -> onDownloadQueued(intent)); - } else if (intent != null && intent.getBooleanExtra(EXTRA_REFRESH_ALL, false)) { - Notification notification = notificationManager.updateNotifications(downloads); - startForeground(R.id.notification_downloading, notification); - setupNotificationUpdaterIfNecessary(); - downloadEnqueueExecutor.execute(() -> enqueueAll(intent)); } else if (downloads.size() == 0) { shutdown(); } else { @@ -198,8 +173,9 @@ public class DownloadService extends Service { boolean showAutoDownloadReport = UserPreferences.showAutoDownloadReport(); if (UserPreferences.showDownloadReport() || showAutoDownloadReport) { - notificationManager.updateReport(reportQueue, showAutoDownloadReport); + notificationManager.updateReport(reportQueue, showAutoDownloadReport, failedRequestsForReport); reportQueue.clear(); + failedRequestsForReport.clear(); } unregisterReceiver(cancelDownloadReceiver); @@ -244,63 +220,16 @@ public class DownloadService extends Service { }); } - /** - * This method MUST NOT, in any case, throw an exception. - * Otherwise, it hangs up the refresh thread pool. - */ - private void performLocalFeedRefresh(Downloader downloader, DownloadRequest request) { - try { - Feed feed = DBReader.getFeed(request.getFeedfileId()); - LocalFeedUpdater.updateFeed(feed, DownloadService.this, (scanned, totalFiles) -> { - request.setSize(totalFiles); - request.setSoFar(scanned); - request.setProgressPercent((int) (100.0 * scanned / totalFiles)); - }); - } catch (Exception e) { - e.printStackTrace(); - } - downloadEnqueueExecutor.submit(() -> { - downloads.remove(downloader); - stopServiceIfEverythingDone(); - }); - } - - private void handleSuccessfulDownload(Downloader downloader) { DownloadRequest request = downloader.getDownloadRequest(); DownloadStatus status = downloader.getResult(); final int type = status.getFeedfileType(); - if (type == Feed.FEEDFILETYPE_FEED) { - Log.d(TAG, "Handling completed Feed Download"); - FeedSyncTask task = new FeedSyncTask(DownloadService.this, request); - boolean success = task.run(); - - if (success) { - if (request.getFeedfileId() == 0) { - return; // No download logs for new subscriptions - } - // we create a 'successful' download log if the feed's last refresh failed - List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); - if (log.size() > 0 && !log.get(0).isSuccessful()) { - saveDownloadStatus(task.getDownloadStatus()); - } - if (!request.isInitiatedByUser()) { - // Was stored in the database before and not initiated manually - newEpisodesNotification.showIfNeeded(DownloadService.this, task.getSavedFeed()); - } - if (downloader.permanentRedirectUrl != null) { - DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl); - } - } else { - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - saveDownloadStatus(task.getDownloadStatus()); - } - } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { Log.d(TAG, "Handling completed FeedMedia Download"); MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, status, request); handler.run(); - saveDownloadStatus(handler.getUpdatedStatus()); + saveDownloadStatus(handler.getUpdatedStatus(), downloader.getDownloadRequest()); } } @@ -319,7 +248,7 @@ public class DownloadService extends Service { DownloadServiceInterface.get().download(this, false, downloader.getDownloadRequest()); } else { Log.e(TAG, "Download failed"); - saveDownloadStatus(status); + saveDownloadStatus(status, downloader.getDownloadRequest()); new FailedDownloadHandler(downloader.getDownloadRequest()).run(); if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { @@ -452,20 +381,6 @@ public class DownloadService extends Service { } } - private void enqueueAll(Intent intent) { - boolean initiatedByUser = intent.getBooleanExtra(EXTRA_INITIATED_BY_USER, false); - List<Feed> feeds = DBReader.getFeedList(); - for (Feed feed : feeds) { - if (feed.getPreferences().getKeepUpdated()) { - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - builder.withInitiatedByUser(initiatedByUser); - addNewRequest(builder.build()); - } - } - postDownloaders(); - stopServiceIfEverythingDone(); - } - private void addNewRequest(@NonNull DownloadRequest request) { if (isDownloadingFile(request.getSource())) { Log.d(TAG, "Skipped enqueueing request. Already running."); @@ -475,17 +390,11 @@ public class DownloadService extends Service { return; } Log.d(TAG, "Add new request: " + request.getSource()); - if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) { - Downloader downloader = new LocalFeedStubDownloader(request); + writeFileUrl(request); + Downloader downloader = downloaderFactory.create(request); + if (downloader != null) { downloads.add(downloader); - downloadHandleExecutor.submit(() -> performLocalFeedRefresh(downloader, request)); - } else { - writeFileUrl(request); - Downloader downloader = downloaderFactory.create(request); - if (downloader != null) { - downloads.add(downloader); - downloadHandleExecutor.submit(() -> performDownload(downloader)); - } + downloadHandleExecutor.submit(() -> performDownload(downloader)); } } @@ -507,8 +416,11 @@ public class DownloadService extends Service { * * @param status the download that is going to be saved */ - private void saveDownloadStatus(@NonNull DownloadStatus status) { + private void saveDownloadStatus(@NonNull DownloadStatus status, @NonNull DownloadRequest request) { reportQueue.add(status); + if (!status.isSuccessful() && !status.isCancelled()) { + failedRequestsForReport.add(request); + } DBWriter.addDownloadStatus(status); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java index 384a6070e..976d8255f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java @@ -5,6 +5,7 @@ import android.content.Intent; import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.util.Log; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; @@ -16,6 +17,13 @@ public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { private static final String TAG = "DownloadServiceInterface"; public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { + Intent intent = makeDownloadIntent(context, cleanupMedia, requests); + if (intent != null) { + ContextCompat.startForegroundService(context, intent); + } + } + + public Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests) { ArrayList<DownloadRequest> requestsToSend = new ArrayList<>(); for (DownloadRequest request : requests) { if (!isDownloadingFile(request.getSource())) { @@ -23,7 +31,7 @@ public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { } } if (requestsToSend.isEmpty()) { - return; + return null; } else if (requestsToSend.size() > 100) { if (BuildConfig.DEBUG) { throw new IllegalArgumentException("Android silently drops intent payloads that are too large"); @@ -38,14 +46,11 @@ public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { if (cleanupMedia) { launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, true); } - ContextCompat.startForegroundService(context, launchIntent); + return launchIntent; } public void refreshAllFeeds(Context context, boolean initiatedByUser) { - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REFRESH_ALL, true); - launchIntent.putExtra(DownloadService.EXTRA_INITIATED_BY_USER, initiatedByUser); - ContextCompat.startForegroundService(context, launchIntent); + FeedUpdateManager.runOnce(context); } public void cancel(Context context, String url) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java index fd24a716e..b9846c06c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -4,9 +4,10 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; +import android.os.Build; import android.util.Log; import androidx.core.app.NotificationCompat; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.util.DownloadErrorLabel; import de.danoeh.antennapod.model.download.DownloadStatus; @@ -14,6 +15,9 @@ import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.ui.appstartintent.DownloadAuthenticationActivityStarter; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import java.util.List; @@ -34,10 +38,9 @@ public class DownloadServiceNotification { .setWhen(0) .setOnlyAlertOnce(true) .setShowWhen(false) - .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(context)) - .setSmallIcon(R.drawable.ic_notification_sync); - notificationCompatBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - + .setContentIntent(getNotificationContentIntent(context)) + .setSmallIcon(R.drawable.ic_notification_sync) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); Log.d(TAG, "Notification set up"); } @@ -58,12 +61,27 @@ public class DownloadServiceNotification { } else { contentTitle = context.getString(R.string.download_notification_title); } - String contentText = (downloads.size() > 0) - ? context.getResources().getQuantityString(R.plurals.downloads_left, downloads.size(), downloads.size()) - : context.getString(R.string.completing); - String bigText = compileNotificationString(downloads); - if (!bigText.contains("\n")) { - contentText = bigText; + + int numDownloads = getNumberOfRunningDownloads(downloads); + String contentText = context.getString(R.string.completing); + String bigText = context.getString(R.string.completing); + notificationCompatBuilder.clearActions(); + if (numDownloads > 0) { + bigText = compileNotificationString(downloads); + if (numDownloads == 1) { + contentText = bigText; + } else { + contentText = context.getResources().getQuantityString(R.plurals.downloads_left, + numDownloads, numDownloads); + } + + Intent cancelDownloadsIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadsIntent.setPackage(context.getPackageName()); + PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(context, + R.id.pending_intent_download_cancel_all, cancelDownloadsIntent, PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + notificationCompatBuilder.addAction(new NotificationCompat.Action( + R.drawable.ic_notification_cancel, context.getString(R.string.cancel_label), cancelPendingIntent)); } notificationCompatBuilder.setContentTitle(contentTitle); @@ -72,6 +90,16 @@ public class DownloadServiceNotification { return notificationCompatBuilder.build(); } + private int getNumberOfRunningDownloads(List<Downloader> downloads) { + int running = 0; + for (Downloader downloader : downloads) { + if (!downloader.cancelled && !downloader.isFinished()) { + running++; + } + } + return running; + } + private boolean typeIsOnly(List<Downloader> downloads, int feedFileType) { for (Downloader downloader : downloads) { if (downloader.cancelled) { @@ -150,7 +178,8 @@ public class DownloadServiceNotification { * user about the number of completed downloads. A report will only be * created if there is at least one failed download excluding images */ - public void updateReport(List<DownloadStatus> reportQueue, boolean showAutoDownloadReport) { + public void updateReport(List<DownloadStatus> reportQueue, boolean showAutoDownloadReport, + List<DownloadRequest> failedRequests) { // check if report should be created boolean createReport = false; int failedDownloads = 0; @@ -170,48 +199,68 @@ public class DownloadServiceNotification { } } - if (createReport) { - Log.d(TAG, "Creating report"); - - // create notification object - String channelId; - int titleId; - int iconId; - int id; - String content; - PendingIntent intent; - if (failedDownloads == 0) { - // We are generating an auto-download report - channelId = NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD; - titleId = R.string.auto_download_report_title; - iconId = R.drawable.ic_notification_new; - intent = ClientConfig.downloadServiceCallbacks.getAutoDownloadReportNotificationContentIntent(context); - id = R.id.notification_auto_download_report; - content = createAutoDownloadNotificationContent(reportQueue); - } else { - channelId = NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR; - titleId = R.string.download_report_title; - iconId = R.drawable.ic_notification_sync_error; - intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context); - id = R.id.notification_download_report; - content = createFailedDownloadNotificationContent(reportQueue); - } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); - builder.setTicker(context.getString(titleId)) - .setContentTitle(context.getString(titleId)) - .setContentText(content) - .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) - .setSmallIcon(iconId) - .setContentIntent(intent) - .setAutoCancel(true); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(id, builder.build()); - Log.d(TAG, "Download report notification was posted"); - } else { + if (!createReport) { Log.d(TAG, "No report is created"); + return; + } + Log.d(TAG, "Creating report"); + if (failedDownloads == 0) { + createAutoDownloadReportNotification(reportQueue); + } else { + createDownloadFailedNotification(reportQueue, failedRequests); } + Log.d(TAG, "Download report notification was posted"); + } + + private void createAutoDownloadReportNotification(List<DownloadStatus> reportQueue) { + PendingIntent intent = getAutoDownloadReportNotificationContentIntent(context); + String content = createAutoDownloadNotificationContent(reportQueue); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, + NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD); + builder.setTicker(context.getString(R.string.auto_download_report_title)) + .setContentTitle(context.getString(R.string.auto_download_report_title)) + .setContentText(content) + .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) + .setSmallIcon(R.drawable.ic_notification_new) + .setContentIntent(intent) + .setAutoCancel(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(R.id.notification_auto_download_report, builder.build()); + } + + private void createDownloadFailedNotification(List<DownloadStatus> reportQueue, + List<DownloadRequest> failedRequests) { + Intent retryIntent = DownloadServiceInterface.get().makeDownloadIntent(context, + false, failedRequests.toArray(new DownloadRequest[0])); + PendingIntent retryPendingIntent = null; + if (retryIntent != null && Build.VERSION.SDK_INT >= 26) { + retryPendingIntent = PendingIntent.getForegroundService(context, R.id.pending_intent_download_service_retry, + retryIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else if (retryIntent != null) { + retryPendingIntent = PendingIntent.getService(context, + R.id.pending_intent_download_service_retry, retryIntent, + PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + PendingIntent intent = getReportNotificationContentIntent(context); + String content = createFailedDownloadNotificationContent(reportQueue); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, + NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); + builder.setTicker(context.getString(R.string.download_report_title)) + .setContentTitle(context.getString(R.string.download_report_title)) + .setContentText(content) + .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) + .setSmallIcon(R.drawable.ic_notification_sync_error) + .setContentIntent(intent) + .setAutoCancel(true); + if (retryPendingIntent != null) { + builder.addAction(new NotificationCompat.Action( + R.drawable.ic_notification_sync, context.getString(R.string.retry_label), retryPendingIntent)); + } + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(R.id.notification_download_report, builder.build()); } public void postAuthenticationNotification(final DownloadRequest downloadRequest) { @@ -226,9 +275,32 @@ public class DownloadServiceNotification { + ": " + resourceTitle)) .setSmallIcon(R.drawable.ic_notification_key) .setAutoCancel(true) - .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(context, downloadRequest)); + .setContentIntent(new DownloadAuthenticationActivityStarter( + context, downloadRequest.getFeedfileId(), downloadRequest).getPendingIntent()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(downloadRequest.getSource().hashCode(), builder.build()); } + + public PendingIntent getReportNotificationContentIntent(Context context) { + Intent intent = new MainActivityStarter(context) + .withFragmentLoaded("DownloadsFragment") + .withFragmentArgs("show_logs", true) + .getIntent(); + return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + + public PendingIntent getAutoDownloadReportNotificationContentIntent(Context context) { + Intent intent = new MainActivityStarter(context).withFragmentLoaded("QueueFragment").getIntent(); + return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + + public PendingIntent getNotificationContentIntent(Context context) { + Intent intent = new MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent(); + return PendingIntent.getActivity(context, + R.id.pending_intent_download_service_notification, intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index a5ed5c3dd..1118b93cd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -43,7 +43,8 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { feed.setId(request.getFeedfileId()); feed.setDownloaded(true); feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, - VolumeAdaptionSetting.OFF, request.getUsername(), request.getPassword())); + VolumeAdaptionSetting.OFF, FeedPreferences.NewEpisodesAction.GLOBAL, request.getUsername(), + request.getPassword())); feed.setPageNr(request.getArguments().getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)); DownloadError reason = null; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 07670bff3..9cb1166b4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -1,40 +1,31 @@ package de.danoeh.antennapod.core.service.download.handler; import android.content.Context; - import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; public class FeedSyncTask { - private final DownloadRequest request; private final Context context; private Feed savedFeed; private final FeedParserTask task; + private FeedHandlerResult feedHandlerResult; public FeedSyncTask(Context context, DownloadRequest request) { - this.request = request; this.context = context; this.task = new FeedParserTask(request); } public boolean run() { - FeedHandlerResult result = task.call(); + feedHandlerResult = task.call(); if (!task.isSuccessful()) { return false; } - savedFeed = DBTasks.updateFeed(context, result.feed, false); - // If loadAllPages=true, check if another page is available and queue it for download - final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequest.REQUEST_ARG_LOAD_ALL_PAGES); - final Feed feed = result.feed; - if (loadAllPages && feed.getNextPageLink() != null) { - feed.setId(savedFeed.getId()); - DBTasks.loadNextPageOfFeed(context, feed, true); - } + savedFeed = DBTasks.updateFeed(context, feedHandlerResult.feed, false); return true; } @@ -46,4 +37,8 @@ public class FeedSyncTask { public Feed getSavedFeed() { return savedFeed; } + + public String getRedirectUrl() { + return feedHandlerResult.redirectUrl; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 897300ca8..c632bf1e0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -62,9 +62,8 @@ public class MediaDownloadedHandler implements Runnable { ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl(), false); } // Get duration - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); String durationStr = null; - try { + try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) { mmr.setDataSource(media.getFile_url()); durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); media.setDuration(Integer.parseInt(durationStr)); @@ -73,8 +72,6 @@ public class MediaDownloadedHandler implements Runnable { Log.d(TAG, "Invalid file duration: " + durationStr); } catch (Exception e) { Log.e(TAG, "Get duration failed", e); - } finally { - mmr.release(); } final FeedItem item = media.getItem(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 9856c617e..e0c5da00b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -40,12 +40,10 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.HttpCredentialEncoder; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.model.playback.Playable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -import org.antennapod.audio.MediaPlayer; import java.util.ArrayList; import java.util.Collections; @@ -53,19 +51,21 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; -public class ExoPlayerWrapper implements IPlayer { +public class ExoPlayerWrapper { + public static final int BUFFERING_STARTED = -1; + public static final int BUFFERING_ENDED = -2; private static final String TAG = "ExoPlayerWrapper"; public static final int ERROR_CODE_OFFSET = 1000; + private final Context context; private final Disposable bufferingUpdateDisposable; private SimpleExoPlayer exoPlayer; private MediaSource mediaSource; - private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener; - private MediaPlayer.OnCompletionListener audioCompletionListener; + private Runnable audioSeekCompleteListener; + private Runnable audioCompletionListener; private Consumer<String> audioErrorListener; - private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener; + private Consumer<Integer> bufferingUpdateListener; private PlaybackParameters playbackParameters; - private MediaPlayer.OnInfoListener infoListener; private DefaultTrackSelector trackSelector; ExoPlayerWrapper(Context context) { @@ -76,7 +76,7 @@ public class ExoPlayerWrapper implements IPlayer { .observeOn(AndroidSchedulers.mainThread()) .subscribe(tickNumber -> { if (bufferingUpdateListener != null) { - bufferingUpdateListener.onBufferingUpdate(null, exoPlayer.getBufferedPercentage()); + bufferingUpdateListener.accept(exoPlayer.getBufferedPercentage()); } }); } @@ -97,11 +97,11 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void onPlaybackStateChanged(@Player.State int playbackState) { if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) { - audioCompletionListener.onCompletion(null); - } else if (infoListener != null && playbackState == Player.STATE_BUFFERING) { - infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); - } else if (infoListener != null) { - infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); + audioCompletionListener.run(); + } else if (bufferingUpdateListener != null && playbackState == Player.STATE_BUFFERING) { + bufferingUpdateListener.accept(BUFFERING_STARTED); + } else if (bufferingUpdateListener != null) { + bufferingUpdateListener.accept(BUFFERING_ENDED); } } @@ -130,28 +130,20 @@ public class ExoPlayerWrapper implements IPlayer { @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) { - audioSeekCompleteListener.onSeekComplete(null); + audioSeekCompleteListener.run(); } } }); } - @Override - public boolean canDownmix() { - return false; - } - - @Override public int getCurrentPosition() { return (int) exoPlayer.getCurrentPosition(); } - @Override public float getCurrentSpeedMultiplier() { return playbackParameters.speed; } - @Override public int getDuration() { if (exoPlayer.getDuration() == C.TIME_UNSET) { return Playable.INVALID_TIME; @@ -159,23 +151,19 @@ public class ExoPlayerWrapper implements IPlayer { return (int) exoPlayer.getDuration(); } - @Override public boolean isPlaying() { return exoPlayer.getPlayWhenReady(); } - @Override public void pause() { exoPlayer.pause(); } - @Override public void prepare() throws IllegalStateException { exoPlayer.setMediaSource(mediaSource, false); exoPlayer.prepare(); } - @Override public void release() { bufferingUpdateDisposable.dispose(); if (exoPlayer != null) { @@ -187,21 +175,18 @@ public class ExoPlayerWrapper implements IPlayer { bufferingUpdateListener = null; } - @Override public void reset() { exoPlayer.release(); createPlayer(); } - @Override public void seekTo(int i) throws IllegalStateException { exoPlayer.seekTo(i); if (audioSeekCompleteListener != null) { - audioSeekCompleteListener.onSeekComplete(null); + audioSeekCompleteListener.run(); } } - @Override public void setAudioStreamType(int i) { AudioAttributes a = exoPlayer.getAudioAttributes(); AudioAttributes.Builder b = new AudioAttributes.Builder(); @@ -235,51 +220,34 @@ public class ExoPlayerWrapper implements IPlayer { mediaSource = f.createMediaSource(mediaItem); } - @Override public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { setDataSource(s, null, null); } - @Override public void setDisplay(SurfaceHolder sh) { exoPlayer.setVideoSurfaceHolder(sh); } - @Override public void setPlaybackParams(float speed, boolean skipSilence) { playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch); exoPlayer.setSkipSilenceEnabled(skipSilence); exoPlayer.setPlaybackParameters(playbackParameters); } - @Override - public void setDownmix(boolean b) { - - } - - @Override public void setVolume(float v, float v1) { exoPlayer.setVolume(v); } - @Override - public void setWakeMode(Context context, int i) { - - } - - @Override public void start() { exoPlayer.play(); // Can't set params when paused - so always set it on start in case they changed exoPlayer.setPlaybackParameters(playbackParameters); } - @Override public void stop() { exoPlayer.stop(); } - @Override public List<String> getAudioTracks() { List<String> trackNames = new ArrayList<>(); TrackNameProvider trackNameProvider = new DefaultTrackNameProvider(context.getResources()); @@ -302,7 +270,6 @@ public class ExoPlayerWrapper implements IPlayer { return formats; } - @Override public void setAudioTrack(int track) { MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); if (trackInfo == null) { @@ -324,7 +291,6 @@ public class ExoPlayerWrapper implements IPlayer { return -1; } - @Override public int getSelectedAudioTrack() { TrackSelectionArray trackSelections = exoPlayer.getCurrentTrackSelections(); List<Format> availableFormats = getFormats(); @@ -340,11 +306,11 @@ public class ExoPlayerWrapper implements IPlayer { return -1; } - void setOnCompletionListener(MediaPlayer.OnCompletionListener audioCompletionListener) { + void setOnCompletionListener(Runnable audioCompletionListener) { this.audioCompletionListener = audioCompletionListener; } - void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener) { + void setOnSeekCompleteListener(Runnable audioSeekCompleteListener) { this.audioSeekCompleteListener = audioSeekCompleteListener; } @@ -366,11 +332,7 @@ public class ExoPlayerWrapper implements IPlayer { return exoPlayer.getVideoFormat().height; } - void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener) { + void setOnBufferingUpdateListener(Consumer<Integer> bufferingUpdateListener) { this.bufferingUpdateListener = bufferingUpdateListener; } - - public void setOnInfoListener(MediaPlayer.OnInfoListener infoListener) { - this.infoListener = infoListener; - } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 6c803d33a..9dc846cc2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -6,47 +6,34 @@ import android.content.res.Configuration; import android.media.AudioManager; import android.os.Handler; import android.os.Looper; -import android.os.PowerManager; -import androidx.annotation.NonNull; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; - +import androidx.annotation.NonNull; import androidx.media.AudioAttributesCompat; import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; +import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.playback.base.PlayerStatus; -import org.antennapod.audio.MediaPlayer; +import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; - -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; -import de.danoeh.antennapod.core.util.playback.AudioPlayer; -import de.danoeh.antennapod.core.util.playback.IPlayer; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.core.util.playback.VideoPlayer; -import org.greenrobot.eventbus.EventBus; /** * Manages the MediaPlayer object of the PlaybackService. @@ -57,7 +44,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final AudioManager audioManager; private volatile PlayerStatus statusBeforeSeeking; - private volatile IPlayer mediaPlayer; + private volatile ExoPlayerWrapper mediaPlayer; private volatile Playable media; private volatile boolean stream; @@ -67,100 +54,15 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private volatile Pair<Integer, Integer> videoSize; private final AudioFocusRequestCompat audioFocusRequest; private final Handler audioFocusCanceller; - - /** - * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads - * have to wait until these operations have finished. - */ - private final PlayerLock playerLock; - private final PlayerExecutor executor; - private boolean useCallerThread = true; private boolean isShutDown = false; - - private CountDownLatch seekLatch; - /** - * All ExoPlayer methods must be executed on the same thread. - * We use the main application thread. This class allows to - * "fake" an executor that just calls the methods on the - * calling thread instead of submitting to an executor. - * Other players are still executed in a background thread. - */ - private class PlayerExecutor { - private ThreadPoolExecutor threadPool; - - public Future<?> submit(Runnable r) { - if (useCallerThread) { - r.run(); - return new FutureTask<Void>(() -> {}, null); - } else { - return threadPool.submit(r); - } - } - - public void shutdown() { - threadPool.shutdown(); - } - } - - /** - * All ExoPlayer methods must be executed on the same thread. - * We use the main application thread. This class allows to - * "fake" a lock that does nothing. A lock is not needed if - * everything is called on the same thread. - * Other players are still executed in a background thread and - * therefore use a real lock. - */ - private class PlayerLock { - private ReentrantLock lock = new ReentrantLock(); - - public void lock() { - if (!useCallerThread) { - lock.lock(); - } - } - - public boolean tryLock(int i, TimeUnit milliseconds) throws InterruptedException { - if (!useCallerThread) { - return lock.tryLock(i, milliseconds); - } - return true; - } - - public boolean tryLock() { - if (!useCallerThread) { - return lock.tryLock(); - } - return true; - } - - public void unlock() { - if (!useCallerThread) { - lock.unlock(); - } - } - - public boolean isHeldByCurrentThread() { - if (!useCallerThread) { - return lock.isHeldByCurrentThread(); - } - return true; - } - } - public LocalPSMP(@NonNull Context context, @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) { super(context, callback); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.playerLock = new PlayerLock(); this.startWhenPrepared = new AtomicBoolean(false); audioFocusCanceller = new Handler(Looper.getMainLooper()); - - executor = new PlayerExecutor(); - executor.threadPool = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<>(), - (r, executor) -> Log.d(TAG, "Rejected execution of runnable")); - mediaPlayer = null; statusBeforeSeeking = null; pausedBecauseOfTransientAudiofocusLoss = false; @@ -207,18 +109,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Log.d(TAG, "playMediaObject(...)"); - useCallerThread = UserPreferences.useExoplayer(); - executor.submit(() -> { - playerLock.lock(); - try { - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } finally { - playerLock.unlock(); - } - }); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } } /** @@ -230,11 +126,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * @see #playMediaObject(Playable, boolean, boolean, boolean) */ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (!playerLock.isHeldByCurrentThread()) { - throw new IllegalStateException("method requires playerLock"); - } - - if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) && playerStatus == PlayerStatus.PLAYING) { @@ -253,7 +144,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (!media.getIdentifier().equals(playable.getIdentifier())) { final Playable oldMedia = media; - executor.submit(() -> callback.onPostPlayback(oldMedia, false, false, true)); + callback.onPostPlayback(oldMedia, false, false, true); } setPlayerStatus(PlayerStatus.INDETERMINATE, null); @@ -313,14 +204,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void resume() { - executor.submit(() -> { - playerLock.lock(); - resumeSync(); - playerLock.unlock(); - }); - } - - private void resumeSync() { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { int focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest); @@ -336,7 +219,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( media.getPosition(), media.getLastPlayedTime()); - seekToSync(newPosition); + seekTo(newPosition); } mediaPlayer.start(); @@ -363,27 +246,22 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void pause(final boolean abandonFocus, final boolean reinit) { - executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Pausing playback."); - mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); - - if (abandonFocus) { - abandonAudioFocus(); - pausedBecauseOfTransientAudiofocusLoss = false; - } - if (stream && reinit) { - reinit(); - } - } else { - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); - } + releaseWifiLockIfNecessary(); + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); - playerLock.unlock(); - }); + if (abandonFocus) { + abandonAudioFocus(); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } } private void abandonAudioFocus() { @@ -398,50 +276,30 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void prepare() { - executor.submit(() -> { - playerLock.lock(); - - if (playerStatus == PlayerStatus.INITIALIZED) { - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - try { - mediaPlayer.prepare(); - onPrepared(startWhenPrepared.get()); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage())); - } - } - playerLock.unlock(); - - }); + if (playerStatus == PlayerStatus.INITIALIZED) { + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } } /** * Called after media player has been prepared. This method is executed on the caller's thread. */ private void onPrepared(final boolean startWhenPrepared) { - playerLock.lock(); - if (playerStatus != PlayerStatus.PREPARING) { - playerLock.unlock(); throw new IllegalStateException("Player is not in PREPARING state"); } - Log.d(TAG, "Resource prepared"); - if (mediaType == MediaType.VIDEO && mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - } else if(mediaType == MediaType.VIDEO && mediaPlayer instanceof VideoPlayer) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); + if (mediaType == MediaType.VIDEO) { + videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); } // TODO this call has no effect! if (media.getPosition() > 0) { - seekToSync(media.getPosition()); + seekTo(media.getPosition()); } if (media.getDuration() <= 0) { @@ -451,10 +309,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PREPARED, media); if (startWhenPrepared) { - resumeSync(); + resume(); } - - playerLock.unlock(); } /** @@ -464,35 +320,28 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void reinit() { - useCallerThread = UserPreferences.useExoplayer(); - executor.submit(() -> { - playerLock.lock(); - Log.d(TAG, "reinit()"); - releaseWifiLockIfNecessary(); - if (media != null) { - playMediaObject(media, true, stream, startWhenPrepared.get(), false); - } else if (mediaPlayer != null) { - mediaPlayer.reset(); - } else { - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); - } - playerLock.unlock(); - }); + Log.d(TAG, "reinit()"); + releaseWifiLockIfNecessary(); + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } } - /** * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * - * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 - * <p/> - * This method is executed on the caller's thread. + * Invalid time values (< 0) will be ignored. + * <p/> + * This method is executed on an internal executor service. */ - private void seekToSync(int t) { + @Override + public void seekTo(int t) { if (t < 0) { t = 0; } - playerLock.lock(); if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -521,18 +370,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { startWhenPrepared.set(false); prepare(); } - playerLock.unlock(); - } - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - * <p/> - * This method is executed on an internal executor service. - */ - @Override - public void seekTo(final int t) { - executor.submit(() -> seekToSync(t)); } /** @@ -542,17 +379,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void seekDelta(final int d) { - executor.submit(() -> { - playerLock.lock(); - int currentPosition = getPosition(); - if (currentPosition != Playable.INVALID_TIME) { - seekToSync(currentPosition + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - - playerLock.unlock(); - }); + int currentPosition = getPosition(); + if (currentPosition != Playable.INVALID_TIME) { + seekTo(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } } /** @@ -560,10 +392,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public int getDuration() { - if (!playerLock.tryLock()) { - return Playable.INVALID_TIME; - } - int retVal = Playable.INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -573,8 +401,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (retVal <= 0 && media != null && media.getDuration() > 0) { retVal = media.getDuration(); } - - playerLock.unlock(); return retVal; } @@ -583,14 +409,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public int getPosition() { - try { - if (!playerLock.tryLock(50, TimeUnit.MILLISECONDS)) { - return Playable.INVALID_TIME; - } - } catch (InterruptedException e) { - return Playable.INVALID_TIME; - } - int retVal = Playable.INVALID_TIME; if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { retVal = mediaPlayer.getCurrentPosition(); @@ -598,8 +416,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (retVal <= 0 && media != null && media.getPosition() >= 0) { retVal = media.getPosition(); } - - playerLock.unlock(); return retVal; } @@ -615,23 +431,13 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { /** * Sets the playback speed. - * This method is executed on the caller's thread. - */ - private void setSpeedSyncAndSkipSilence(float speed, boolean skipSilence) { - playerLock.lock(); - Log.d(TAG, "Playback speed was set to " + speed); - EventBus.getDefault().post(new SpeedChangedEvent(speed)); - mediaPlayer.setPlaybackParams(speed, skipSilence); - playerLock.unlock(); - } - - /** - * Sets the playback speed. * This method is executed on an internal executor service. */ @Override public void setPlaybackParams(final float speed, final boolean skipSilence) { - executor.submit(() -> setSpeedSyncAndSkipSilence(speed, skipSilence)); + Log.d(TAG, "Playback speed was set to " + speed); + EventBus.getDefault().post(new SpeedChangedEvent(speed)); + mediaPlayer.setPlaybackParams(speed, skipSilence); } /** @@ -639,10 +445,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public float getPlaybackSpeed() { - if (!playerLock.tryLock()) { - return 1; - } - float retVal = 1; if ((playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -650,7 +452,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { || playerStatus == PlayerStatus.PREPARED)) { retVal = mediaPlayer.getCurrentSpeedMultiplier(); } - playerLock.unlock(); return retVal; } @@ -659,16 +460,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * This method is executed on an internal executor service. */ @Override - public void setVolume(final float volumeLeft, float volumeRight) { - executor.submit(() -> setVolumeSync(volumeLeft, volumeRight)); - } - - /** - * Sets the playback volume. - * This method is executed on the caller's thread. - */ - private void setVolumeSync(float volumeLeft, float volumeRight) { - playerLock.lock(); + public void setVolume(float volumeLeft, float volumeRight) { Playable playable = getPlayable(); if (playable instanceof FeedMedia) { FeedMedia feedMedia = (FeedMedia) playable; @@ -680,29 +472,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } mediaPlayer.setVolume(volumeLeft, volumeRight); Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight); - playerLock.unlock(); - } - - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - @Override - public boolean canDownmix() { - boolean retVal = false; - if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { - retVal = mediaPlayer.canDownmix(); - } - return retVal; - } - - @Override - public void setDownmix(boolean enable) { - playerLock.lock(); - if (media != null && media.getMediaType() == MediaType.AUDIO) { - mediaPlayer.setDownmix(enable); - Log.d(TAG, "Media player downmix was set to " + enable); - } - playerLock.unlock(); } @Override @@ -734,35 +503,26 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { playerStatus = PlayerStatus.STOPPED; } isShutDown = true; - executor.shutdown(); abandonAudioFocus(); releaseWifiLockIfNecessary(); } @Override public void setVideoSurface(final SurfaceHolder surface) { - executor.submit(() -> { - playerLock.lock(); - if (mediaPlayer != null) { - mediaPlayer.setDisplay(surface); - } - playerLock.unlock(); - }); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } } @Override public void resetVideoSurface() { - executor.submit(() -> { - playerLock.lock(); - if (mediaType == MediaType.VIDEO) { - Log.d(TAG, "Resetting video surface"); - mediaPlayer.setDisplay(null); - reinit(); - } else { - Log.e(TAG, "Resetting video surface for media of Audio type"); - } - playerLock.unlock(); - }); + if (mediaType == MediaType.VIDEO) { + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + } else { + Log.e(TAG, "Resetting video surface for media of Audio type"); + } } /** @@ -774,24 +534,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public Pair<Integer, Integer> getVideoSize() { - if (!playerLock.tryLock()) { - // use cached value if lock can't be aquired - return videoSize; - } - Pair<Integer, Integer> res; - if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { - res = null; - } else if (mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; - } else { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; + if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) { + videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); } - playerLock.unlock(); - return res; + return videoSize; } /** @@ -832,16 +578,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { return; } - if (UserPreferences.useExoplayer()) { - mediaPlayer = new ExoPlayerWrapper(context); - } else if (media.getMediaType() == MediaType.VIDEO) { - mediaPlayer = new VideoPlayer(); - } else { - mediaPlayer = new AudioPlayer(context); - } - + mediaPlayer = new ExoPlayerWrapper(context); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); setMediaPlayerListeners(mediaPlayer); } @@ -858,103 +596,94 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { return; } - executor.submit(() -> { - playerLock.lock(); - if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK - && !UserPreferences.shouldPauseForFocusLoss()) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - setVolumeSync(0.25f, 0.25f); - pausedBecauseOfTransientAudiofocusLoss = false; - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT - || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - mediaPlayer.pause(); // Pause without telling the PlaybackService - pausedBecauseOfTransientAudiofocusLoss = true; - - audioFocusCanceller.removeCallbacksAndMessages(null); - audioFocusCanceller.postDelayed(() -> { - if (pausedBecauseOfTransientAudiofocusLoss) { - // Still did not get back the audio focus. Now actually pause. - pause(true, false); - } - }, 30000); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - Log.d(TAG, "Gained audio focus"); - audioFocusCanceller.removeCallbacksAndMessages(null); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - mediaPlayer.start(); - } else { // we ducked => raise audio level back - setVolumeSync(1.0f, 1.0f); - } + if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + && !UserPreferences.shouldPauseForFocusLoss()) { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + setVolume(0.25f, 0.25f); pausedBecauseOfTransientAudiofocusLoss = false; } - playerLock.unlock(); - }); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT + || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + mediaPlayer.pause(); // Pause without telling the PlaybackService + pausedBecauseOfTransientAudiofocusLoss = true; + + audioFocusCanceller.removeCallbacksAndMessages(null); + audioFocusCanceller.postDelayed(() -> { + if (pausedBecauseOfTransientAudiofocusLoss) { + // Still did not get back the audio focus. Now actually pause. + pause(true, false); + } + }, 30000); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + Log.d(TAG, "Gained audio focus"); + audioFocusCanceller.removeCallbacksAndMessages(null); + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + mediaPlayer.start(); + } else { // we ducked => raise audio level back + setVolume(1.0f, 1.0f); + } + pausedBecauseOfTransientAudiofocusLoss = false; + } } }; @Override - protected Future<?> endPlayback(final boolean hasEnded, final boolean wasSkipped, + protected void endPlayback(final boolean hasEnded, final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) { - useCallerThread = UserPreferences.useExoplayer(); - return executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - - // we're relying on the position stored in the Playable object for post-playback processing - if (media != null) { - int position = getPosition(); - if (position >= 0) { - media.setPosition(position); - } - } + releaseWifiLockIfNecessary(); - if (mediaPlayer != null) { - mediaPlayer.reset(); - } + boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - abandonAudioFocus(); - - final Playable currentMedia = media; - Playable nextMedia = null; - - if (shouldContinue) { - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - nextMedia = callback.getNextInQueue(currentMedia); - if (nextMedia != null) { - callback.onPlaybackEnded(nextMedia.getMediaType(), false); - // setting media to null signals to playMediaObject() that - // we're taking care of post-playback processing - media = null; - playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying); - } + // we're relying on the position stored in the Playable object for post-playback processing + if (media != null) { + int position = getPosition(); + if (position >= 0) { + media.setPosition(position); } - if (shouldContinue || toStoppedState) { - if (nextMedia == null) { - callback.onPlaybackEnded(null, true); - stop(); - } - final boolean hasNext = nextMedia != null; + } - executor.submit(() -> callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)); - } else if (isPlaying) { - callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + if (mediaPlayer != null) { + mediaPlayer.reset(); + } + + abandonAudioFocus(); + + final Playable currentMedia = media; + Playable nextMedia = null; + + if (shouldContinue) { + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + nextMedia = callback.getNextInQueue(currentMedia); + if (nextMedia != null) { + callback.onPlaybackEnded(nextMedia.getMediaType(), false); + // setting media to null signals to playMediaObject() that + // we're taking care of post-playback processing + media = null; + playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying); } - playerLock.unlock(); - }); + } + if (shouldContinue || toStoppedState) { + if (nextMedia == null) { + callback.onPlaybackEnded(null, true); + stop(); + } + final boolean hasNext = nextMedia != null; + + callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext); + } else if (isPlaying) { + callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + } } /** @@ -964,165 +693,55 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * abandoning audio focus have to be done with other methods. */ private void stop() { - executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - playerLock.unlock(); + releaseWifiLockIfNecessary(); - }); + if (playerStatus == PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.STOPPED, null); + } else { + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + } } @Override - protected boolean shouldLockWifi(){ + protected boolean shouldLockWifi() { return stream; } - private void setMediaPlayerListeners(IPlayer mp) { + private void setMediaPlayerListeners(ExoPlayerWrapper mp) { if (mp == null || media == null) { return; } - if (mp instanceof VideoPlayer) { - if (media.getMediaType() != MediaType.VIDEO) { - Log.w(TAG, "video player, but media type is " + media.getMediaType()); - } - VideoPlayer vp = (VideoPlayer) mp; - vp.setOnCompletionListener(videoCompletionListener); - vp.setOnSeekCompleteListener(videoSeekCompleteListener); - vp.setOnErrorListener(videoErrorListener); - vp.setOnBufferingUpdateListener(videoBufferingUpdateListener); - vp.setOnInfoListener(videoInfoListener); - } else if (mp instanceof AudioPlayer) { - if (media.getMediaType() != MediaType.AUDIO) { - Log.w(TAG, "audio player, but media type is " + media.getMediaType()); - } - AudioPlayer ap = (AudioPlayer) mp; - ap.setOnCompletionListener(audioCompletionListener); - ap.setOnSeekCompleteListener(audioSeekCompleteListener); - ap.setOnErrorListener(audioErrorListener); - ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); - ap.setOnInfoListener(audioInfoListener); - } else if (mp instanceof ExoPlayerWrapper) { - ExoPlayerWrapper ap = (ExoPlayerWrapper) mp; - ap.setOnCompletionListener(audioCompletionListener); - ap.setOnSeekCompleteListener(audioSeekCompleteListener); - ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); - ap.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); - ap.setOnInfoListener(audioInfoListener); - } else { - Log.w(TAG, "Unknown media player: " + mp); - } - } - - private void clearMediaPlayerListeners() { - if (mediaPlayer instanceof VideoPlayer) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - vp.setOnCompletionListener(x -> { }); - vp.setOnSeekCompleteListener(x -> { }); - vp.setOnErrorListener((mediaPlayer, i, i1) -> false); - vp.setOnBufferingUpdateListener((mediaPlayer, i) -> { }); - vp.setOnInfoListener((mediaPlayer, i, i1) -> false); - } else if (mediaPlayer instanceof AudioPlayer) { - AudioPlayer ap = (AudioPlayer) mediaPlayer; - ap.setOnCompletionListener(x -> { }); - ap.setOnSeekCompleteListener(x -> { }); - ap.setOnErrorListener((x, y, z) -> false); - ap.setOnBufferingUpdateListener((arg0, percent) -> { }); - ap.setOnInfoListener((arg0, what, extra) -> false); - } else if (mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer; - ap.setOnCompletionListener(x -> { }); - ap.setOnSeekCompleteListener(x -> { }); - ap.setOnBufferingUpdateListener((arg0, percent) -> { }); - ap.setOnErrorListener(x -> { }); - ap.setOnInfoListener((arg0, what, extra) -> false); - } - } - - private final MediaPlayer.OnCompletionListener audioCompletionListener = - mp -> genericOnCompletion(); - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = - mp -> genericOnCompletion(); - - private void genericOnCompletion() { - endPlayback(true, false, true, true); - } - - private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = - (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = - (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); - - private final MediaPlayer.OnInfoListener audioInfoListener = - (mp, what, extra) -> genericInfoListener(what); - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = - (mp, what, extra) -> genericInfoListener(what); - - private boolean genericInfoListener(int what) { - switch (what) { - case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START: + mp.setOnCompletionListener(() -> endPlayback(true, false, true, true)); + mp.setOnSeekCompleteListener(this::genericSeekCompleteListener); + mp.setOnBufferingUpdateListener(percent -> { + if (percent == ExoPlayerWrapper.BUFFERING_STARTED) { EventBus.getDefault().post(BufferUpdateEvent.started()); - return true; - case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END: + } else if (percent == ExoPlayerWrapper.BUFFERING_ENDED) { EventBus.getDefault().post(BufferUpdateEvent.ended()); - return true; - default: - return true; - } + } else { + EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); + } + }); + mp.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); } - private final MediaPlayer.OnErrorListener audioErrorListener = - (mp, what, extra) -> { - if(mp != null && mp.canFallback()) { - mp.fallback(); - return true; - } else { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError; - - private boolean genericOnError(Object inObj, int what, int extra) { - EventBus.getDefault().postSticky(new PlayerErrorEvent(MediaPlayerError.getErrorString(context, what))); - return true; + private void clearMediaPlayerListeners() { + mediaPlayer.setOnCompletionListener(() -> { }); + mediaPlayer.setOnSeekCompleteListener(() -> { }); + mediaPlayer.setOnBufferingUpdateListener(percent -> { }); + mediaPlayer.setOnErrorListener(x -> { }); } - private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = - mp -> genericSeekCompleteListener(); - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = - mp -> genericSeekCompleteListener(); - private void genericSeekCompleteListener() { Log.d(TAG, "genericSeekCompleteListener"); if (seekLatch != null) { seekLatch.countDown(); } - - Runnable r = () -> { - playerLock.lock(); - if (playerStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(media, getPosition()); - } - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media, getPosition()); - } - playerLock.unlock(); - }; - - if (useCallerThread) { - r.run(); - } else { - executor.submit(r); + if (playerStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(media, getPosition()); + } + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media, getPosition()); } } 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 92119e31e..42631296b 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 @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.playback; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; +import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -13,7 +14,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.media.AudioManager; import android.net.Uri; @@ -43,38 +43,23 @@ import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.media.MediaBrowserServiceCompat; -import androidx.preference.PreferenceManager; -import de.danoeh.antennapod.core.service.QuickSettingsTileService; -import de.danoeh.antennapod.core.util.playback.PlayableUtils; -import de.danoeh.antennapod.event.playback.BufferUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.cast.CastPsmp; -import de.danoeh.antennapod.playback.cast.CastStateListener; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; +import java.util.GregorianCalendar; import java.util.List; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; -import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; -import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.service.QuickSettingsTileService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.FeedSearcher; @@ -83,14 +68,31 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.widget.WidgetUpdater; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.event.PlayerErrorEvent; +import de.danoeh.antennapod.event.playback.BufferUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; +import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; +import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.cast.CastPsmp; +import de.danoeh.antennapod.playback.cast.CastStateListener; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; import io.reactivex.Completable; @@ -114,10 +116,13 @@ public class PlaybackService extends MediaBrowserServiceCompat { private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; /** - * Custom action used by Android Wear, Android Auto + * Custom actions used by Android Wear, Android Auto, and Android (API 33+ only) */ + private static final String CUSTOM_ACTION_SKIP_TO_NEXT = "action.de.danoeh.antennapod.core.service.skipToNext"; private static final String CUSTOM_ACTION_FAST_FORWARD = "action.de.danoeh.antennapod.core.service.fastForward"; private static final String CUSTOM_ACTION_REWIND = "action.de.danoeh.antennapod.core.service.rewind"; + private static final String CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = + "action.de.danoeh.antennapod.core.service.changePlaybackSpeed"; /** * Set a max number of episodes to load for Android Auto, otherwise there could be performance issues @@ -217,9 +222,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { EventBus.getDefault().register(this); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - PreferenceManager.getDefaultSharedPreferences(this) - .registerOnSharedPreferenceChangeListener(prefListener); - recreateMediaSessionIfNeeded(); castStateListener = new CastStateListener(this) { @Override @@ -298,7 +300,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { castStateListener.destroy(); cancelPositionObserver(); - PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener); if (mediaSession != null) { mediaSession.release(); mediaSession = null; @@ -310,6 +311,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { unregisterReceiver(audioBecomingNoisy); mediaPlayer.shutdown(); taskManager.shutdown(); + EventBus.getDefault().unregister(this); } @Override @@ -401,9 +403,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>(); if (parentId.equals(getResources().getString(R.string.app_name))) { mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black, - DBReader.getQueue().size())); + DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.QUEUED)))); mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, - DBReader.getDownloadedItems().size())); + DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)))); mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black, DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.UNPLAYED)))); List<Feed> feeds = DBReader.getFeedList(); @@ -417,11 +419,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (parentId.equals(getResources().getString(R.string.queue_label))) { feedItems = DBReader.getQueue(); } else if (parentId.equals(getResources().getString(R.string.downloads_label))) { - feedItems = DBReader.getDownloadedItems(); + feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), UserPreferences.getDownloadsSortedOrder()); } else if (parentId.equals(getResources().getString(R.string.episodes_label))) { - feedItems = DBReader.getRecentlyPublishedEpisodes(0, - MAX_ANDROID_AUTO_EPISODES_PER_FEED, - new FeedItemFilter(FeedItemFilter.UNPLAYED)); + feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, + new FeedItemFilter(FeedItemFilter.UNPLAYED), SortOrder.DATE_NEW_OLD); } else if (parentId.startsWith("FeedId:")) { long feedId = Long.parseLong(parentId.split(":")[1]); feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId)); @@ -552,6 +554,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { } } + @SuppressLint("LaunchActivityFromNotification") private void displayStreamingNotAllowedNotification(Intent originalIntent) { Intent intentAllowThisTime = new Intent(originalIntent); intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME); @@ -777,8 +780,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { updateMediaSession(newInfo.playerStatus); switch (newInfo.playerStatus) { case INITIALIZED: - PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable, - mediaPlayer.getPSMPInfo().playerStatus); + if (mediaPlayer.getPSMPInfo().playable != null) { + PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable, + mediaPlayer.getPSMPInfo().playerStatus); + } updateNotificationAndMediaSession(newInfo.playable); break; case PREPARED: @@ -805,8 +810,18 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.validStartCommandWasReceived(); stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()); // set sleep timer if auto-enabled + boolean autoEnableByTime = true; + int fromSetting = SleepTimerPreferences.autoEnableFrom(); + int toSetting = SleepTimerPreferences.autoEnableTo(); + if (fromSetting != toSetting) { + Calendar now = new GregorianCalendar(); + now.setTimeInMillis(System.currentTimeMillis()); + int currentHour = now.get(Calendar.HOUR_OF_DAY); + autoEnableByTime = SleepTimerPreferences.isInTimeRange(fromSetting, toSetting, currentHour); + } + if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING - && SleepTimerPreferences.autoEnable() && !sleepTimerActive()) { + && SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) { setSleepTimer(SleepTimerPreferences.timerMillis()); EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label), PlaybackService.this::disableSleepTimer)); @@ -910,7 +925,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { mediaPlayer.pause(true, false); } - PlaybackPreferences.writeNoMediaPlaying(); stateManager.stopService(); } @@ -1083,13 +1097,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { // Delete episode if enabled FeedPreferences.AutoDeleteAction action = item.getFeed().getPreferences().getCurrentAutoDelete(); - boolean shouldAutoDelete = action == FeedPreferences.AutoDeleteAction.YES + boolean shouldAutoDelete = action == FeedPreferences.AutoDeleteAction.ALWAYS || (action == FeedPreferences.AutoDeleteAction.GLOBAL && UserPreferences.isAutoDelete()); if (shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) || !UserPreferences.shouldFavoriteKeepEpisode())) { DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); Log.d(TAG, "Episode Deleted"); } + notifyChildrenChanged(getString(R.string.queue_label)); } } @@ -1182,45 +1197,52 @@ public class PlaybackService extends MediaBrowserServiceCompat { } else { state = PlaybackStateCompat.STATE_NONE; } + sessionState.setState(state, getCurrentPosition(), getCurrentPlaybackSpeed()); - long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE + long capabilities = PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED; - UiModeManager uiModeManager = (UiModeManager) getApplicationContext() - .getSystemService(Context.UI_MODE_SERVICE); - if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { - sessionState.addCustomAction( + sessionState.setActions(capabilities); + + // On Android Auto, custom actions are added in the following order around the play button, if no default + // actions are present: Near left, near right, far left, far right, additional actions panel + PlaybackStateCompat.CustomAction.Builder rewindBuilder = new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_REWIND, + getString(R.string.rewind_label), + R.drawable.ic_notification_fast_rewind + ); + WearMediaSession.addWearExtrasToAction(rewindBuilder); + sessionState.addCustomAction(rewindBuilder.build()); + + PlaybackStateCompat.CustomAction.Builder fastForwardBuilder = new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_FAST_FORWARD, + getString(R.string.fast_forward_label), + R.drawable.ic_notification_fast_forward + ); + WearMediaSession.addWearExtrasToAction(fastForwardBuilder); + sessionState.addCustomAction(fastForwardBuilder.build()); + + sessionState.addCustomAction( new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_REWIND, - getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind) - .build()); - sessionState.addCustomAction( + CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, + getString(R.string.playback_speed), + R.drawable.ic_notification_playback_speed + ).build() + ); + sessionState.addCustomAction( new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_FAST_FORWARD, - getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) - .build()); - } else { - // This would give the PIP of videos a play button - capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY; - if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) { - WearMediaSession.sessionStateAddActionForWear(sessionState, - CUSTOM_ACTION_REWIND, - getString(R.string.rewind_label), - android.R.drawable.ic_media_rew); - WearMediaSession.sessionStateAddActionForWear(sessionState, - CUSTOM_ACTION_FAST_FORWARD, - getString(R.string.fast_forward_label), - android.R.drawable.ic_media_ff); - WearMediaSession.mediaSessionSetExtraForWear(mediaSession); - } - } + CUSTOM_ACTION_SKIP_TO_NEXT, + getString(R.string.skip_episode_label), + R.drawable.ic_notification_skip + ).build() + ); - sessionState.setActions(capabilities); + WearMediaSession.mediaSessionSetExtraForWear(mediaSession); mediaSession.setPlaybackState(sessionState.build()); } @@ -1243,11 +1265,25 @@ public class PlaybackService extends MediaBrowserServiceCompat { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()); - if (UserPreferences.setLockscreenBackground() && notificationBuilder.isIconCached()) { + + if (notificationBuilder.isIconCached()) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.getCachedIcon()); - } else if (isCasting && !TextUtils.isEmpty(p.getImageLocation())) { - // In the absence of metadata art, the controller dialog takes care of creating it. - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, p.getImageLocation()); + } else { + String iconUri = p.getImageLocation(); + if (p instanceof FeedMedia) { // Don't use embedded cover etc, which Android can't load + FeedMedia m = (FeedMedia) p; + if (m.getItem() != null) { + FeedItem item = m.getItem(); + if (item.getImageUrl() != null) { + iconUri = item.getImageUrl(); + } else if (item.getFeed() != null) { + iconUri = item.getFeed().getImageUrl(); + } + } + } + if (!TextUtils.isEmpty(iconUri)) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri); + } } if (stateManager.hasReceivedValidStartCommand()) { @@ -1561,6 +1597,13 @@ public class PlaybackService extends MediaBrowserServiceCompat { } public void setSpeed(float speed) { + PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed); + if (currentMediaType == MediaType.VIDEO) { + UserPreferences.setVideoPlaybackSpeed(speed); + } else { + UserPreferences.setPlaybackSpeed(speed); + } + mediaPlayer.setPlaybackParams(speed, UserPreferences.isSkipSilence()); } @@ -1575,14 +1618,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getPlaybackSpeed(); } - public boolean canDownmix() { - return mediaPlayer.canDownmix(); - } - - public void setDownmix(boolean enable) { - mediaPlayer.setDownmix(enable); - } - public boolean isStartWhenPrepared() { return mediaPlayer.isStartWhenPrepared(); } @@ -1680,6 +1715,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (playable instanceof FeedMedia) { long itemId = ((FeedMedia) playable).getItem().getId(); DBWriter.addQueueItem(this, false, true, itemId); + notifyChildrenChanged(getString(R.string.queue_label)); } } @@ -1800,14 +1836,27 @@ public class PlaybackService extends MediaBrowserServiceCompat { onFastForward(); } else if (CUSTOM_ACTION_REWIND.equals(action)) { onRewind(); + } else if (CUSTOM_ACTION_SKIP_TO_NEXT.equals(action)) { + mediaPlayer.skip(); + } else if (CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED.equals(action)) { + List<Float> selectedSpeeds = UserPreferences.getPlaybackSpeedArray(); + + // If the list has zero or one element, there's nothing we can do to change the playback speed. + if (selectedSpeeds.size() > 1) { + int speedPosition = selectedSpeeds.indexOf(mediaPlayer.getPlaybackSpeed()); + float newSpeed; + + if (speedPosition == selectedSpeeds.size() - 1) { + // This is the last element. Wrap instead of going over the size of the list. + newSpeed = selectedSpeeds.get(0); + } else { + // If speedPosition is still -1 (the user isn't using a preset), use the first preset in the + // list. + newSpeed = selectedSpeeds.get(speedPosition + 1); + } + onSetPlaybackSpeed(newSpeed); + } } } }; - - private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = - (sharedPreferences, key) -> { - if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { - updateNotificationAndMediaSession(getPlayable()); - } - }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 3111d01bc..3d69dbdbc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -294,6 +294,7 @@ public class PlaybackServiceTaskManager { public void run() { Log.d(TAG, "Starting"); long lastTick = System.currentTimeMillis(); + EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)); while (timeLeft > 0) { try { Thread.sleep(UPDATE_INTERVAL); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java index 2be330a3e..1d2e3ffd2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -14,7 +14,9 @@ import java.util.Locale; import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.SortOrder; /** * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod. @@ -88,7 +90,8 @@ public class APCleanupAlgorithm extends EpisodeCleanupAlgorithm { @NonNull private List<FeedItem> getCandidates() { List<FeedItem> candidates = new ArrayList<>(); - List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD); Date mostRecentDateForDeletion = calcMostRecentDateForDeletion(new Date()); for (FeedItem item : downloadedItems) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java index 9fce34a44..2bd840fd1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java @@ -12,6 +12,8 @@ import java.util.Locale; import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; /** * A cleanup algorithm that removes any item that isn't in the queue and isn't a favorite @@ -75,7 +77,8 @@ public class APQueueCleanupAlgorithm extends EpisodeCleanupAlgorithm { @NonNull private List<FeedItem> getCandidates() { List<FeedItem> candidates = new ArrayList<>(); - List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD); for (FeedItem item : downloadedItems) { if (item.hasMedia() && item.getMedia().isDownloaded() diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 5fbbbbc19..0f3121551 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; @@ -52,7 +54,8 @@ public class AutomaticDownloadAlgorithm { List<FeedItem> candidates; final List<FeedItem> queue = DBReader.getQueue(); - final List<FeedItem> newItems = DBReader.getNewItemsList(0, Integer.MAX_VALUE); + final List<FeedItem> newItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.NEW), SortOrder.DATE_NEW_OLD); candidates = new ArrayList<>(queue.size() + newItems.size()); candidates.addAll(queue); for (FeedItem newItem : newItems) { @@ -76,7 +79,7 @@ public class AutomaticDownloadAlgorithm { } int autoDownloadableEpisodes = candidates.size(); - int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); int deletedEpisodes = EpisodeCleanupAlgorithmFactory.build() .makeRoomForEpisodes(context, autoDownloadableEpisodes); boolean cacheIsUnlimited = diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 55acb8b92..94a7334f3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -1,11 +1,12 @@ package de.danoeh.antennapod.core.storage; import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; -import android.text.TextUtils; -import android.util.Log; import java.util.ArrayList; import java.util.Collections; @@ -14,26 +15,27 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; +import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.feed.SubscriptionsFilter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.storage.database.PodDBAdapter; -import de.danoeh.antennapod.storage.database.mapper.DownloadStatusCursorMapper; import de.danoeh.antennapod.storage.database.mapper.ChapterCursorMapper; +import de.danoeh.antennapod.storage.database.mapper.DownloadStatusCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedItemCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedMediaCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedPreferencesCursorMapper; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; +import de.danoeh.antennapod.storage.preferences.UserPreferences; /** * Provides methods for reading data from the AntennaPod database. @@ -274,69 +276,6 @@ public final class DBReader { } } - /** - * Loads a list of FeedItems whose episode has been downloaded. - * - * @return A list of FeedItems whose episdoe has been downloaded. - */ - @NonNull - public static List<FeedItem> getDownloadedItems() { - Log.d(TAG, "getDownloadedItems() called"); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try (Cursor cursor = adapter.getDownloadedItemsCursor()) { - List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); - loadAdditionalFeedItemListData(items); - Collections.sort(items, new FeedItemPubdateComparator()); - return items; - } finally { - adapter.close(); - } - } - - /** - * Loads a list of FeedItems whose episode has been played. - * - * @return A list of FeedItems whose episdoe has been played. - */ - @NonNull - public static List<FeedItem> getPlayedItems() { - Log.d(TAG, "getPlayedItems() called"); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try (Cursor cursor = adapter.getPlayedItemsCursor()) { - List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); - loadAdditionalFeedItemListData(items); - return items; - } finally { - adapter.close(); - } - } - - /** - * Loads a list of FeedItems that are considered new. - * Excludes items from feeds that do not have keep updated enabled. - * - * @param offset The first episode that should be loaded. - * @param limit The maximum number of episodes that should be loaded. - * @return A list of FeedItems that are considered new. - */ - public static List<FeedItem> getNewItemsList(int offset, int limit) { - Log.d(TAG, "getNewItemsList() called"); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try (Cursor cursor = adapter.getNewItemsCursor(offset, limit)) { - List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); - loadAdditionalFeedItemListData(items); - return items; - } finally { - adapter.close(); - } - } - private static LongList getFavoriteIDList() { Log.d(TAG, "getFavoriteIDList() called"); @@ -354,19 +293,17 @@ public final class DBReader { } /** - * Loads a filtered list of FeedItems sorted by pubDate in descending order. * * @param offset The first episode that should be loaded. * @param limit The maximum number of episodes that should be loaded. * @param filter The filter describing which episodes to filter out. */ @NonNull - public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit, FeedItemFilter filter) { + public static List<FeedItem> getEpisodes(int offset, int limit, FeedItemFilter filter, SortOrder sortOrder) { Log.d(TAG, "getRecentlyPublishedEpisodes() called with: offset=" + offset + ", limit=" + limit); - PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit, filter)) { + try (Cursor cursor = adapter.getEpisodesCursor(offset, limit, filter, sortOrder)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; @@ -375,26 +312,26 @@ public final class DBReader { } } - public static List<FeedItem> getRandomEpisodes(int limit, int seed) { + public static int getTotalEpisodeCount(FeedItemFilter filter) { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getRandomEpisodesCursor(limit, seed)) { - List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); - loadAdditionalFeedItemListData(items); - return items; + try (Cursor cursor = adapter.getEpisodeCountCursor(filter)) { + if (cursor.moveToFirst()) { + return cursor.getInt(0); + } + return -1; } finally { adapter.close(); } } - public static int getTotalEpisodeCount(FeedItemFilter filter) { + public static List<FeedItem> getRandomEpisodes(int limit, int seed) { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getTotalEpisodeCountCursor(filter)) { - if (cursor.moveToFirst()) { - return cursor.getInt(0); - } - return -1; + try (Cursor cursor = adapter.getRandomEpisodesCursor(limit, seed)) { + List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); + loadAdditionalFeedItemListData(items); + return items; } finally { adapter.close(); } @@ -751,24 +688,6 @@ public final class DBReader { } /** - * Returns the number of downloaded episodes. - * - * @return The number of downloaded episodes. - */ - - public static int getNumberOfDownloadedEpisodes() { - Log.d(TAG, "getNumberOfDownloadedEpisodes() called with: " + ""); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try { - return adapter.getNumberOfDownloadedEpisodes(); - } finally { - adapter.close(); - } - } - - /** * Searches the DB for a FeedMedia of the given id. * * @param mediaId The id of the object @@ -893,14 +812,17 @@ public final class DBReader { * items. */ @NonNull - public static NavDrawerData getNavDrawerData() { + public static NavDrawerData getNavDrawerData(@Nullable SubscriptionsFilter subscriptionsFilter) { Log.d(TAG, "getNavDrawerData() called with: " + ""); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final Map<Long, Integer> feedCounters = adapter.getFeedCounters(UserPreferences.getFeedCounterSetting()); - SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); - List<Feed> feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters); + List<Feed> feeds = getFeedList(adapter); + + if (subscriptionsFilter != null) { + feeds = subscriptionsFilter.filter(feeds, feedCounters); + } Comparator<Feed> comparator; int feedOrder = UserPreferences.getFeedOrder(); @@ -954,9 +876,9 @@ public final class DBReader { } Collections.sort(feeds, comparator); - int queueSize = adapter.getQueueSize(); - int numNewItems = adapter.getNumberOfNewItems(); - int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes(); + final int queueSize = adapter.getQueueSize(); + final int numNewItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)); + final int numDownloadedItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); List<NavDrawerData.DrawerItem> items = new ArrayList<>(); Map<String, NavDrawerData.TagDrawerItem> folders = new HashMap<>(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 423c83c79..8b79d594c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -1,20 +1,28 @@ package de.danoeh.antennapod.core.storage; -import static android.content.Context.MODE_PRIVATE; - import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.VisibleForTesting; - -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.storage.database.PodDBAdapter; import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; @@ -29,30 +37,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - /** * Provides methods for doing common tasks that use DBReader and DBWriter. */ public final class DBTasks { private static final String TAG = "DBTasks"; - private static final String PREF_NAME = "dbtasks"; - private static final String PREF_LAST_REFRESH = "last_refresh"; - /** * Executor service used by the autodownloadUndownloadedEpisodes method. */ @@ -103,68 +93,12 @@ public final class DBTasks { } } - /** - * Refreshes all feeds. - * It must not be from the main thread. - * This method might ignore subsequent calls if it is still - * enqueuing Feeds for download from a previous call - * - * @param context Might be used for accessing the database - * @param initiatedByUser a boolean indicating if the refresh was triggered by user action. - */ - public static void refreshAllFeeds(final Context context, boolean initiatedByUser) { - DownloadServiceInterface.get().refreshAllFeeds(context, initiatedByUser); - - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); - prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); - - SynchronizationQueueSink.syncNow(); - // Note: automatic download of episodes will be done but not here. - // Instead it is done after all feeds have been refreshed (asynchronously), - // in DownloadService.onDestroy() - // See Issue #2577 for the details of the rationale - } - - - - /** - * Queues the next page of this Feed for download. The given Feed has to be a paged - * Feed (isPaged()=true) and must contain a nextPageLink. - * - * @param context Used for requesting the download. - * @param feed The feed whose next page should be loaded. - * @param loadAllPages True if any subsequent pages should also be loaded, false otherwise. - */ - public static void loadNextPageOfFeed(final Context context, Feed feed, boolean loadAllPages) { - if (feed.isPaged() && feed.getNextPageLink() != null) { - int pageNr = feed.getPageNr() + 1; - Feed nextFeed = new Feed(feed.getNextPageLink(), null, feed.getTitle() + "(" + pageNr + ")"); - nextFeed.setPageNr(pageNr); - nextFeed.setPaged(true); - nextFeed.setId(feed.getId()); - - DownloadRequest.Builder builder = DownloadRequestCreator.create(nextFeed); - builder.loadAllPages(loadAllPages); - DownloadServiceInterface.get().download(context, false, builder.build()); - } else { - Log.e(TAG, "loadNextPageOfFeed: Feed was either not paged or contained no nextPageLink"); - } - } - public static void forceRefreshFeed(Context context, Feed feed, boolean initiatedByUser) { forceRefreshFeed(context, feed, false, initiatedByUser); } - public static void forceRefreshCompleteFeed(final Context context, final Feed feed) { - forceRefreshFeed(context, feed, true, true); - } - private static void forceRefreshFeed(Context context, Feed feed, boolean loadAllPages, boolean initiatedByUser) { - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - builder.withInitiatedByUser(initiatedByUser); - builder.setForce(true); - builder.loadAllPages(loadAllPages); - DownloadServiceInterface.get().download(context, false, builder.build()); + FeedUpdateManager.runOnce(context, feed); } /** @@ -298,13 +232,6 @@ public final class DBTasks { Log.d(TAG, "Found no existing Feed with title " + newFeed.getTitle() + ". Adding as new one."); - // Add a new Feed - // all new feeds will have the most recent item marked as unplayed - FeedItem mostRecent = newFeed.getMostRecentItem(); - if (mostRecent != null) { - mostRecent.setNew(); - } - resultFeed = newFeed; } else { Log.d(TAG, "Feed with title " + newFeed.getTitle() @@ -328,7 +255,7 @@ public final class DBTasks { // get the most recent date now, before we start changing the list FeedItem priorMostRecent = savedFeed.getMostRecentItem(); - Date priorMostRecentDate = null; + Date priorMostRecentDate = new Date(); if (priorMostRecent != null) { priorMostRecentDate = priorMostRecent.getPubDate(); } @@ -388,14 +315,15 @@ public final class DBTasks { savedFeed.getItems().add(idx, item); } - // only mark the item new if it was published after or at the same time - // as the most recent item - // (if the most recent date is null then we can assume there are no items - // and this is the first, hence 'new') - // New items that do not have a pubDate set are always marked as new - if (item.getPubDate() == null || priorMostRecentDate == null - || priorMostRecentDate.before(item.getPubDate()) - || priorMostRecentDate.equals(item.getPubDate())) { + FeedPreferences.NewEpisodesAction action = savedFeed.getPreferences().getNewEpisodesAction(); + if (action == FeedPreferences.NewEpisodesAction.GLOBAL) { + action = UserPreferences.getNewEpisodesAction(); + } + if (action == FeedPreferences.NewEpisodesAction.ADD_TO_INBOX + && (item.getPubDate() == null + || priorMostRecentDate == null + || priorMostRecentDate.before(item.getPubDate()) + || priorMostRecentDate.equals(item.getPubDate()))) { Log.d(TAG, "Marking item published on " + item.getPubDate() + " new, prior most recent date = " + priorMostRecentDate); item.setNew(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 9b4146f15..dcee8a45a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -665,6 +665,15 @@ public class DBWriter { adapter.close(); } + public static Future<?> resetPagedFeedPage(Feed feed) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.resetPagedFeedPage(feed); + adapter.close(); + }); + } + /* * Sets the 'read'-attribute of all specified FeedItems * @@ -698,7 +707,6 @@ public class DBWriter { }); } - /** * Sets the 'read'-attribute of a FeedItem to the specified value. * diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java index 84a0dd575..e6d3b2917 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.storage.preferences.UserPreferences; public abstract class EpisodeCleanupAlgorithm { @@ -51,8 +52,7 @@ public abstract class EpisodeCleanupAlgorithm { int getNumEpisodesToCleanup(final int amountOfRoomNeeded) { if (amountOfRoomNeeded >= 0 && UserPreferences.getEpisodeCacheSize() != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) { - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(); + int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); if (downloadedEpisodes + amountOfRoomNeeded >= UserPreferences .getEpisodeCacheSize()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java index 5d092da6d..da01a82f1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java @@ -13,6 +13,8 @@ import java.util.Locale; import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.storage.preferences.UserPreferences; /** @@ -74,7 +76,8 @@ public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm { @NonNull private List<FeedItem> getCandidates() { List<FeedItem> candidates = new ArrayList<>(); - List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD); for (FeedItem item : downloadedItems) { if (item.hasMedia() && item.getMedia().isDownloaded() @@ -89,7 +92,7 @@ public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm { public int getDefaultCleanupParameter() { int cacheSize = UserPreferences.getEpisodeCacheSize(); if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) { - int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); if (downloadedEpisodes > cacheSize) { return downloadedEpisodes - cacheSize; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java index 23d308c02..b81f281e8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.List; +import java.util.Random; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.model.feed.FeedItem; @@ -46,6 +47,9 @@ class ItemEnqueuePositionCalculator { int currentlyPlayingPosition = getCurrentlyPlayingPosition(curQueue, currentPlaying); return getPositionOfFirstNonDownloadingItem( currentlyPlayingPosition + 1, curQueue); + case RANDOM: + Random random = new Random(); + return random.nextInt(curQueue.size() + 1); default: throw new AssertionError("calcPosition() : unrecognized enqueueLocation option: " + enqueueLocation); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java index 184f24793..f616b9c4e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java @@ -25,8 +25,6 @@ public class EpisodeActionFilter { Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode()); switch (remoteAction.getAction()) { case NEW: - remoteActionsThatOverrideLocalActions.put(key, remoteAction); - break; case DOWNLOAD: break; case PLAY: @@ -72,7 +70,8 @@ public class EpisodeActionFilter { EpisodeAction secondAction) { return secondAction != null && secondAction.getTimestamp() != null - && secondAction.getTimestamp().after(firstAction.getTimestamp()); + && (firstAction.getTimestamp() == null + || secondAction.getTimestamp().after(firstAction.getTimestamp())); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index c4a9ec581..7fe0c5e46 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -20,14 +20,15 @@ import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -151,9 +152,10 @@ public class SyncService extends Worker { continue; } if (!UrlChecker.containsUrl(localSubscriptions, downloadUrl) && !queuedRemovedFeeds.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, null); - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - DownloadServiceInterface.get().download(getApplicationContext(), false, builder.build()); + Feed feed = new Feed(downloadUrl, null, "Unknown podcast"); + feed.setItems(Collections.emptyList()); + Feed newFeed = DBTasks.updateFeed(getApplicationContext(), feed, false); + FeedUpdateManager.runOnce(getApplicationContext(), newFeed); } } @@ -191,9 +193,13 @@ public class SyncService extends Worker { private void waitForDownloadServiceCompleted() { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_wait_for_downloads)); try { - while (DownloadService.isRunning) { + while (true) { //noinspection BusyWait Thread.sleep(1000); + FeedUpdateRunningEvent event = EventBus.getDefault().getStickyEvent(FeedUpdateRunningEvent.class); + if (event == null || !event.isFeedUpdateRunning) { + return; + } } } catch (InterruptedException e) { e.printStackTrace(); @@ -213,7 +219,8 @@ public class SyncService extends Worker { List<EpisodeAction> queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); if (lastSync == 0) { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); - List<FeedItem> readItems = DBReader.getPlayedItems(); + List<FeedItem> readItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD); Log.d(TAG, "First sync. Upload state for all " + readItems.size() + " played episodes"); for (FeedItem item : readItems) { FeedMedia media = item.getMedia(); @@ -267,10 +274,6 @@ public class SyncService extends Worker { Log.i(TAG, "Feed item has no media: " + action); continue; } - if (action.getAction() == EpisodeAction.NEW) { - DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true); - continue; - } feedItem.getMedia().setPosition(action.getPosition() * 1000); if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) { Log.d(TAG, "Marking as played: " + action); @@ -325,7 +328,7 @@ public class SyncService extends Worker { private static OneTimeWorkRequest.Builder getWorkRequest() { Constraints.Builder constraints = new Constraints.Builder(); - if (UserPreferences.isAllowMobileFeedRefresh()) { + if (UserPreferences.isAllowMobileSync()) { constraints.setRequiredNetworkType(NetworkType.CONNECTED); } else { constraints.setRequiredNetworkType(NetworkType.UNMETERED); diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java index 5c6d58fe3..e1e373953 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java @@ -94,13 +94,15 @@ public class SynchronizationQueueStorage { protected void enqueueFeedAdded(String downloadUrl) { SharedPreferences sharedPreferences = getSharedPreferences(); - String json = sharedPreferences - .getString(QUEUED_FEEDS_ADDED, "[]"); try { - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - sharedPreferences - .edit().putString(QUEUED_FEEDS_ADDED, queue.toString()).apply(); + JSONArray addedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_ADDED, "[]")); + addedQueue.put(downloadUrl); + JSONArray removedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]")); + removedQueue.remove(indexOf(downloadUrl, removedQueue)); + sharedPreferences.edit() + .putString(QUEUED_FEEDS_ADDED, addedQueue.toString()) + .putString(QUEUED_FEEDS_REMOVED, removedQueue.toString()) + .apply(); } catch (JSONException jsonException) { jsonException.printStackTrace(); @@ -109,17 +111,33 @@ public class SynchronizationQueueStorage { protected void enqueueFeedRemoved(String downloadUrl) { SharedPreferences sharedPreferences = getSharedPreferences(); - String json = sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]"); try { - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - sharedPreferences.edit().putString(QUEUED_FEEDS_REMOVED, queue.toString()) + JSONArray removedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]")); + removedQueue.put(downloadUrl); + JSONArray addedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_ADDED, "[]")); + addedQueue.remove(indexOf(downloadUrl, addedQueue)); + sharedPreferences.edit() + .putString(QUEUED_FEEDS_ADDED, addedQueue.toString()) + .putString(QUEUED_FEEDS_REMOVED, removedQueue.toString()) .apply(); } catch (JSONException jsonException) { jsonException.printStackTrace(); } } + private int indexOf(String string, JSONArray array) { + try { + for (int i = 0; i < array.length(); i++) { + if (array.getString(i).equals(string)) { + return i; + } + } + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + return -1; + } + protected void enqueueEpisodeAction(EpisodeAction action) { SharedPreferences sharedPreferences = getSharedPreferences(); String json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java deleted file mode 100644 index 0602fc4fe..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java +++ /dev/null @@ -1,167 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.FeedUpdateWorker; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.util.NetworkUtils; - -public class AutoUpdateManager { - private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; - private static final String WORK_ID_FEED_UPDATE_ONCE = WORK_ID_FEED_UPDATE + "Once"; - private static final String TAG = "AutoUpdateManager"; - - private AutoUpdateManager() { - - } - - /** - * Start / restart periodic auto feed refresh - * @param context Context - */ - public static void restartUpdateAlarm(Context context) { - if (UserPreferences.isAutoUpdateDisabled()) { - disableAutoUpdate(context); - } else if (UserPreferences.isAutoUpdateTimeOfDay()) { - int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); - restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1], context); - } else { - long milliseconds = UserPreferences.getUpdateInterval(); - restartUpdateIntervalAlarm(milliseconds, context); - } - } - - /** - * Sets the interval in which the feeds are refreshed automatically - */ - private static void restartUpdateIntervalAlarm(long intervalMillis, Context context) { - Log.d(TAG, "Restarting update alarm."); - - PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(FeedUpdateWorker.class, - intervalMillis, TimeUnit.MILLISECONDS) - .setConstraints(getConstraints()) - .build(); - - WorkManager.getInstance(context).enqueueUniquePeriodicWork( - WORK_ID_FEED_UPDATE, ExistingPeriodicWorkPolicy.REPLACE, workRequest); - } - - /** - * Sets time of day the feeds are refreshed automatically - */ - private static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute, Context context) { - Log.d(TAG, "Restarting update alarm."); - - Calendar now = Calendar.getInstance(); - Calendar alarm = (Calendar) now.clone(); - alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); - alarm.set(Calendar.MINUTE, minute); - if (alarm.before(now) || alarm.equals(now)) { - alarm.add(Calendar.DATE, 1); - } - long triggerAtMillis = alarm.getTimeInMillis() - now.getTimeInMillis(); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) - .setConstraints(getConstraints()) - .setInitialDelay(triggerAtMillis, TimeUnit.MILLISECONDS) - .build(); - - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE, - ExistingWorkPolicy.REPLACE, workRequest); - } - - /** - * Run auto feed refresh once in background, as soon as what OS scheduling allows. - * - * Callers from UI should use {@link #runImmediate(Context)}, as it will guarantee - * the refresh be run immediately. - * @param context Context - */ - public static void runOnce(Context context) { - Log.d(TAG, "Run auto update once, as soon as OS allows."); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) - .setConstraints(getConstraints()) - .setInitialDelay(0L, TimeUnit.MILLISECONDS) - .setInputData(new Data.Builder() - .putBoolean(FeedUpdateWorker.PARAM_RUN_ONCE, true) - .build() - ) - .build(); - - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_ONCE, - ExistingWorkPolicy.REPLACE, workRequest); - - } - - /** - /** - * Run auto feed refresh once in background immediately, using its own thread. - * - * Callers where the additional threads is not suitable should use {@link #runOnce(Context)} - */ - public static void runImmediate(@NonNull Context context) { - Log.d(TAG, "Run auto update immediately in background."); - if (!NetworkUtils.networkAvailable()) { - Log.d(TAG, "Ignoring: No network connection."); - } else if (NetworkUtils.isFeedRefreshAllowed()) { - startRefreshAllFeeds(context); - } else { - confirmMobileAllFeedsRefresh(context); - } - } - - private static void confirmMobileAllFeedsRefresh(final Context context) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.feed_refresh_title) - .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message) - .setPositiveButton(R.string.confirm_mobile_streaming_button_once, - (dialog, which) -> startRefreshAllFeeds(context)) - .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { - UserPreferences.setAllowMobileFeedRefresh(true); - startRefreshAllFeeds(context); - }) - .setNegativeButton(R.string.no, null); - builder.show(); - } - - private static void startRefreshAllFeeds(final Context context) { - new Thread(() -> DBTasks.refreshAllFeeds( - context.getApplicationContext(), true), "ManualRefreshAllFeeds").start(); - } - - public static void disableAutoUpdate(Context context) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); - } - - private static Constraints getConstraints() { - Constraints.Builder constraints = new Constraints.Builder(); - - if (UserPreferences.isAllowMobileFeedRefresh()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - return constraints.build(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java new file mode 100644 index 000000000..d1a273d4e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.core.util.download; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.FeedUpdateWorker; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +import java.util.concurrent.TimeUnit; + +public class FeedUpdateManager { + public static final String WORK_TAG_FEED_UPDATE = "feedUpdate"; + private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; + private static final String WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"; + public static final String EXTRA_FEED_ID = "feed_id"; + public static final String EXTRA_NEXT_PAGE = "next_page"; + private static final String TAG = "AutoUpdateManager"; + + private FeedUpdateManager() { + + } + + /** + * Start / restart periodic auto feed refresh + * @param context Context + */ + public static void restartUpdateAlarm(Context context, boolean replace) { + if (UserPreferences.isAutoUpdateDisabled()) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); + } else { + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder( + FeedUpdateWorker.class, UserPreferences.getUpdateInterval(), TimeUnit.HOURS) + .setConstraints(getConstraints()) + .build(); + WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, + replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest); + } + } + + public static void runOnce(Context context) { + runOnce(context, null, false); + } + + public static void runOnce(Context context, Feed feed) { + runOnce(context, feed, false); + } + + public static void runOnce(Context context, Feed feed, boolean nextPage) { + OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) + .setInitialDelay(0L, TimeUnit.MILLISECONDS) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(WORK_TAG_FEED_UPDATE); + if (feed != null) { + Data.Builder builder = new Data.Builder(); + builder.putLong(EXTRA_FEED_ID, feed.getId()); + builder.putBoolean(EXTRA_NEXT_PAGE, nextPage); + workRequest.setInputData(builder.build()); + } + WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, + ExistingWorkPolicy.REPLACE, workRequest.build()); + } + + public static void runOnceOrAsk(@NonNull Context context) { + Log.d(TAG, "Run auto update immediately in background."); + if (!NetworkUtils.networkAvailable()) { + Log.d(TAG, "Ignoring: No network connection."); + } else if (NetworkUtils.isFeedRefreshAllowed()) { + runOnce(context); + } else { + confirmMobileAllFeedsRefresh(context); + } + } + + private static void confirmMobileAllFeedsRefresh(final Context context) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setTitle(R.string.feed_refresh_title) + .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message) + .setPositiveButton(R.string.confirm_mobile_streaming_button_once, + (dialog, which) -> runOnce(context)) + .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { + UserPreferences.setAllowMobileFeedRefresh(true); + runOnce(context); + }) + .setNegativeButton(R.string.no, null); + builder.show(); + } + + private static Constraints getConstraints() { + Constraints.Builder constraints = new Constraints.Builder(); + + if (UserPreferences.isAllowMobileFeedRefresh()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + return constraints.build(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java deleted file mode 100644 index 4f6b2ce3a..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import androidx.preference.PreferenceManager; -import android.util.Log; -import android.view.SurfaceHolder; - -import de.danoeh.antennapod.core.ClientConfig; -import org.antennapod.audio.MediaPlayer; - -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class AudioPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "AudioPlayer"; - - public AudioPlayer(Context context) { - super(context, true, ClientConfig.USER_AGENT); - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> { - if (UserPreferences.PREF_MEDIA_PLAYER.equals(key)) { - checkMpi(); - } - }); - } - - @Override - public void setDisplay(SurfaceHolder sh) { - if (sh != null) { - Log.e(TAG, "Setting display not supported in Audio Player"); - throw new UnsupportedOperationException("Setting display not supported in Audio Player"); - } - } - - @Override - public void setPlaybackParams(float speed, boolean skipSilence) { - if (canSetSpeed()) { - try { - setPlaybackSpeed(speed); - } catch (Exception e) { - e.printStackTrace(); - } - } - //Default player does not support silence skipping - } - - @Override - protected boolean useSonic() { - return UserPreferences.useSonic(); - } - - @Override - protected boolean downmix() { - return UserPreferences.stereoToMono(); - } - - public List<String> getAudioTracks() { - return Collections.emptyList(); - } - - @Override - public void setAudioTrack(int track) { - } - - @Override - public int getSelectedAudioTrack() { - return -1; - } - - @Override - public void setDataSource(String streamUrl, String username, String password) throws IOException { - setDataSource(streamUrl); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java deleted file mode 100644 index c726c5b5e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import android.view.SurfaceHolder; - -import java.io.IOException; -import java.util.List; - -public interface IPlayer { - boolean canDownmix(); - - int getCurrentPosition(); - - float getCurrentSpeedMultiplier(); - - int getDuration(); - - boolean isPlaying(); - - void pause(); - - void prepare() throws IllegalStateException, IOException; - - void release(); - - void reset(); - - void seekTo(int msec); - - void setAudioStreamType(int streamtype); - - void setDataSource(String path) throws IllegalStateException, IOException, - IllegalArgumentException, SecurityException; - - void setDataSource(String streamUrl, String username, String password) throws IOException; - - void setDisplay(SurfaceHolder sh); - - void setPlaybackParams(float speed, boolean skipSilence); - - void setDownmix(boolean enable); - - void setVolume(float left, float right); - - void start(); - - void stop(); - - void setWakeMode(Context context, int mode); - - List<String> getAudioTracks(); - - void setAudioTrack(int track); - - int getSelectedAudioTrack(); -} 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 7a0fcdc4c..2274dcf83 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,7 +21,6 @@ import de.danoeh.antennapod.event.playback.SpeedChangedEvent; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; @@ -399,13 +398,6 @@ public abstract class PlaybackController { } public void setPlaybackSpeed(float speed) { - PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed); - if (getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO) { - UserPreferences.setVideoPlaybackSpeed(speed); - } else { - UserPreferences.setPlaybackSpeed(speed); - } - if (playbackService != null) { playbackService.setSpeed(speed); } else { @@ -427,17 +419,6 @@ public abstract class PlaybackController { } } - public boolean canDownmix() { - return (playbackService != null && playbackService.canDownmix()) - || UserPreferences.useSonic(); - } - - public void setDownmix(boolean enable) { - if (playbackService != null) { - playbackService.setDownmix(enable); - } - } - public List<String> getAudioTracks() { if (playbackService == null) { return Collections.emptyList(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java deleted file mode 100644 index ecf47f8ae..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.media.MediaPlayer; -import android.util.Log; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class VideoPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "VideoPlayer"; - - @Override - public boolean canDownmix() { - return false; - } - - @Override - public float getCurrentSpeedMultiplier() { - return 1; - } - - @Override - public void setPlaybackParams(float speed, boolean skipSilence) { - //Ignore this for non ExoPlayer implementations - } - - @Override - public void setDownmix(boolean b) { - Log.e(TAG, "Setting downmix unsupported in video player"); - throw new UnsupportedOperationException("Setting downmix unsupported in video player"); - } - - @Override - public void setVideoScalingMode(int mode) { - super.setVideoScalingMode(mode); - } - - public List<String> getAudioTracks() { - return Collections.emptyList(); - } - - @Override - public void setAudioTrack(int track) { - } - - @Override - public int getSelectedAudioTrack() { - return -1; - } - - @Override - public void setDataSource(String streamUrl, String username, String password) throws IOException { - setDataSource(streamUrl); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java index dda8a37fb..b27325cf8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java @@ -176,6 +176,9 @@ public abstract class WidgetUpdater { views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE); views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE); views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE); + } else { + views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.GONE); + views.setInt(R.id.butPlay, "setVisibility", View.VISIBLE); } int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); diff --git a/core/src/main/res/drawable/bg_pill_translucent.xml b/core/src/main/res/drawable/bg_pill_translucent.xml new file mode 100644 index 000000000..b25a9ac82 --- /dev/null +++ b/core/src/main/res/drawable/bg_pill_translucent.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <solid android:color="#D2404040" /> + <corners android:radius="18dp" /> +</shape> diff --git a/core/src/main/res/drawable/ic_network.xml b/core/src/main/res/drawable/ic_network.xml deleted file mode 100644 index 52f5889b8..000000000 --- a/core/src/main/res/drawable/ic_network.xml +++ /dev/null @@ -1,5 +0,0 @@ -<vector android:height="24dp" - android:viewportHeight="24.0" android:viewportWidth="24.0" - android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="?attr/action_icon_color" android:pathData="M16,17.01V10h-2v7.01h-3L15,21l4,-3.99h-3zM9,3L5,6.99h3V14h2V6.99h3L9,3z"/> -</vector> diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index b08541771..7749c6f3c 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -2,7 +2,7 @@ <resources> <string-array name="spnAutoDeleteItems"> - <item>@string/feed_auto_download_global</item> + <item>@string/global_default</item> <item>@string/feed_auto_download_always</item> <item>@string/feed_auto_download_never</item> </string-array> @@ -25,6 +25,50 @@ <item>heavy</item> </string-array> + <string-array name="feed_refresh_interval_entries"> + <item>@string/feed_refresh_never</item> + <item>@string/feed_every_hour</item> + <item>@string/feed_every_2_hours</item> + <item>@string/feed_every_4_hours</item> + <item>@string/feed_every_8_hours</item> + <item>@string/feed_every_12_hours</item> + <item>@string/feed_every_24_hours</item> + <item>@string/feed_every_72_hours</item> + </string-array> + + <string-array name="feed_refresh_interval_values"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>4</item> + <item>8</item> + <item>12</item> + <item>24</item> + <item>72</item> + </string-array> + + <string-array name="globalNewEpisodesActionItems"> + <item>@string/feed_new_episodes_action_add_to_inbox</item> + <item>@string/feed_new_episodes_action_nothing</item> + </string-array> + + <string-array name="globalNewEpisodesActionValues"> + <item>1</item> + <item>2</item> + </string-array> + + <string-array name="feedNewEpisodesActionItems"> + <item>@string/global_default</item> + <item>@string/feed_new_episodes_action_add_to_inbox</item> + <item>@string/feed_new_episodes_action_nothing</item> + </string-array> + + <string-array name="feedNewEpisodesActionValues"> + <item>0</item> + <item>1</item> + <item>2</item> + </string-array> + <string-array name="smart_mark_as_played_values"> <item>0</item> <item>15</item> @@ -71,6 +115,7 @@ <item>@string/pref_mobileUpdate_auto_download</item> <item>@string/pref_mobileUpdate_streaming</item> <item>@string/pref_mobileUpdate_images</item> + <item>@string/synchronization_pref</item> </string-array> <string-array name="mobile_update_values"> @@ -79,10 +124,12 @@ <item>auto_download</item> <item>streaming</item> <item>images</item> + <item>sync</item> </string-array> <string-array name="mobile_update_default_value"> <item>images</item> + <item>sync</item> </string-array> <string-array name="episode_cleanup_entries"> @@ -114,6 +161,7 @@ <item>@string/enqueue_location_back</item> <item>@string/enqueue_location_front</item> <item>@string/enqueue_location_after_current</item> + <item>@string/enqueue_location_random</item> </string-array> <string-array name="enqueue_location_values"> @@ -121,6 +169,7 @@ <item>BACK</item> <item>FRONT</item> <item>AFTER_CURRENTLY_PLAYING</item> + <item>RANDOM</item> </string-array> <string-array name="episode_cleanup_values"> @@ -135,20 +184,6 @@ <item>-2</item> </string-array> - <string-array name="theme_options"> - <item>@string/pref_theme_title_use_system</item> - <item>@string/pref_theme_title_light</item> - <item>@string/pref_theme_title_dark</item> - <item>@string/pref_theme_title_trueblack</item> - </string-array> - - <string-array name="theme_values"> - <item>system</item> - <item>0</item> - <item>1</item> - <item>2</item> - </string-array> - <string-array name="nav_drawer_titles"> <item>@string/home_label</item> <item>@string/queue_label</item> @@ -205,18 +240,6 @@ <item>DownloadsSection</item> </string-array> - <string-array name="media_player_options"> - <item>@string/media_player_exoplayer_recommended</item> - <item>@string/media_player_builtin</item> - <item>@string/media_player_sonic</item> - </string-array> - - <string-array name="media_player_values"> - <item>exoplayer</item> - <item>builtin</item> - <item>sonic</item> - </string-array> - <!-- sort for podcast screen, not for queue --> <string-array name="feed_episodes_sort_options"> <item>@string/sort_date_new_old</item> diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 19a7c0fde..553da121a 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -9,7 +9,7 @@ <color name="black">#000000</color> <color name="image_readability_tint">#80000000</color> <color name="feed_image_bg">#50000000</color> - <color name="feed_text_bg">#ccbfbfbf</color> + <color name="feed_text_bg">#55333333</color> <!-- Theme colors --> <color name="background_light">#FFFFFF</color> diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 87046cc0f..90d143d38 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -19,6 +19,7 @@ <item name="notification_gpodnet_sync_error" type="id"/> <item name="notification_gpodnet_sync_autherror" type="id"/> <item name="notification_downloading" type="id"/> + <item name="notification_updating_feeds" type="id"/> <item name="notification_download_report" type="id"/> <item name="notification_auto_download_report" type="id"/> <item name="notification_playing" type="id"/> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 6ebef0bb2..6e1335706 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -286,4 +286,13 @@ <item name="android:clickable">true</item> </style> + <style name="TextPill"> + <item name="android:background">@drawable/bg_pill_translucent</item> + <item name="android:layout_margin">8dp</item> + <item name="android:textColor">@color/white</item> + <item name="android:textAlignment">center</item> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingEnd">8dp</item> + </style> + </resources> diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java index 8df05d10d..47881da0b 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java @@ -6,21 +6,20 @@ import android.support.v4.media.session.PlaybackStateCompat; import android.support.wearable.media.MediaControlConstants; public class WearMediaSession { - static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, - CharSequence name, int icon) { - PlaybackStateCompat.CustomAction.Builder actionBuilder = - new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon); + /** + * Take a custom action builder and make sure the custom action shows on Wear OS because this is the Play version + * of the app. + */ + static void addWearExtrasToAction(PlaybackStateCompat.CustomAction.Builder actionBuilder) { Bundle actionExtras = new Bundle(); actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true); actionBuilder.setExtras(actionExtras); - - sessionState.addCustomAction(actionBuilder.build()); } static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { Bundle sessionExtras = new Bundle(); - sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true); - sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true); + sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, false); + sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, false); mediaSession.setExtras(sessionExtras); } } diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java index a04884cac..7c877103f 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java @@ -26,6 +26,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowMediaMetadataRetriever; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -262,7 +263,11 @@ public class LocalFeedUpdaterTest { // call method to test Feed feed = new Feed(FEED_URL, null); - LocalFeedUpdater.tryUpdateFeed(feed, context, null, null); + try { + LocalFeedUpdater.tryUpdateFeed(feed, context, null, null); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java index 93f158b66..b67bc48ea 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java @@ -10,9 +10,12 @@ import java.util.List; import java.util.Random; import androidx.test.platform.app.InstrumentationRegistry; + import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.storage.database.PodDBAdapter; @@ -237,7 +240,8 @@ public class DbReaderTest { public void testGetDownloadedItems() { final int numItems = 10; List<FeedItem> downloaded = saveDownloadedItems(numItems); - List<FeedItem> downloadedSaved = DBReader.getDownloadedItems(); + List<FeedItem> downloadedSaved = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD); assertNotNull(downloadedSaved); assertEquals(downloaded.size(), downloadedSaved.size()); for (FeedItem item : downloadedSaved) { @@ -281,7 +285,8 @@ public class DbReaderTest { for (int i = 0; i < newItems.size(); i++) { unreadIds[i] = newItems.get(i).getId(); } - List<FeedItem> newItemsSaved = DBReader.getNewItemsList(0, Integer.MAX_VALUE); + List<FeedItem> newItemsSaved = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.NEW), SortOrder.DATE_NEW_OLD); assertNotNull(newItemsSaved); assertEquals(newItemsSaved.size(), newItems.size()); for (FeedItem feedItem : newItemsSaved) { @@ -325,7 +330,7 @@ public class DbReaderTest { final int numFeeds = 10; final int numItems = 10; DbTestUtils.saveFeedlist(numFeeds, numItems, true); - NavDrawerData navDrawerData = DBReader.getNavDrawerData(); + NavDrawerData navDrawerData = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()); assertEquals(numFeeds, navDrawerData.items.size()); assertEquals(0, navDrawerData.numNewItems); assertEquals(0, navDrawerData.queueSize); @@ -354,7 +359,7 @@ public class DbReaderTest { adapter.close(); - NavDrawerData navDrawerData = DBReader.getNavDrawerData(); + NavDrawerData navDrawerData = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter()); assertEquals(numFeeds, navDrawerData.items.size()); assertEquals(numNew, navDrawerData.numNewItems); assertEquals(numQueue, navDrawerData.queueSize); diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java index 94695ca95..1f638bf32 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java @@ -184,4 +184,29 @@ public class EpisodeActionFilterTest extends TestCase { .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); assertEquals(0, uniqueList.size()); } -}
\ No newline at end of file + + public void testPresentRemoteTimestampOverridesMissingLocalTimestamp() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date arbitraryTime = format.parse("2021-01-01 08:00:00"); + + List<EpisodeAction> episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + // no timestamp + .position(10) + .build() + ); + + List<EpisodeAction> remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(arbitraryTime) + .position(10) + .build() + ); + + Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(1, uniqueList.size()); + } +} diff --git a/event/src/main/java/de/danoeh/antennapod/event/FeedUpdateRunningEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FeedUpdateRunningEvent.java new file mode 100644 index 000000000..4c14c1647 --- /dev/null +++ b/event/src/main/java/de/danoeh/antennapod/event/FeedUpdateRunningEvent.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.event; + +public class FeedUpdateRunningEvent { + public final boolean isFeedUpdateRunning; + + public FeedUpdateRunningEvent(boolean isRunning) { + this.isFeedUpdateRunning = isRunning; + } +} diff --git a/gradle.properties b/gradle.properties index 4217706ec..e57f3d564 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ android.useAndroidX=true android.enableJetifier=true -android.jetifier.blacklist=bcprov-jdk15on +android.jetifier.ignorelist=bcprov-jdk15on org.gradle.jvmargs=-Xmx4096m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b..8049c684f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java index 006505eb1..94a4776a5 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java @@ -173,7 +173,8 @@ public class Feed extends FeedFile { */ public Feed(String url, String lastUpdate, String title, String username, String password) { this(url, lastUpdate, title); - preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password); + preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, + FeedPreferences.NewEpisodesAction.GLOBAL, username, password); } /** diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java index 2fb1a5c0c..9e8583075 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java @@ -128,6 +128,9 @@ public class FeedMedia extends FeedFile implements Playable { if (other.size > 0) { size = other.size; } + if (other.duration > 0 && duration <= 0) { // Do not overwrite duration that we measured after downloading + duration = other.duration; + } if (other.mime_type != null) { mime_type = other.mime_type; } @@ -145,6 +148,9 @@ public class FeedMedia extends FeedFile implements Playable { if (other.size > 0 && other.size != size) { return true; } + if (other.duration > 0 && duration <= 0) { + return true; + } return false; } diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java index cbbb3c2e7..8e3dd48f6 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java @@ -17,9 +17,45 @@ public class FeedPreferences implements Serializable { public static final String TAG_SEPARATOR = "\u001e"; public enum AutoDeleteAction { - GLOBAL, - YES, - NO + GLOBAL(0), + ALWAYS(1), + NEVER(2); + + public final int code; + + AutoDeleteAction(int code) { + this.code = code; + } + + public static AutoDeleteAction fromCode(int code) { + for (AutoDeleteAction action : values()) { + if (code == action.code) { + return action; + } + } + return NEVER; + } + } + + public enum NewEpisodesAction { + GLOBAL(0), + ADD_TO_INBOX(1), + NOTHING(2); + + public final int code; + + NewEpisodesAction(int code) { + this.code = code; + } + + public static NewEpisodesAction fromCode(int code) { + for (NewEpisodesAction action : values()) { + if (code == action.code) { + return action; + } + } + return ADD_TO_INBOX; + } } @NonNull @@ -29,6 +65,7 @@ public class FeedPreferences implements Serializable { private boolean keepUpdated; private AutoDeleteAction autoDeleteAction; private VolumeAdaptionSetting volumeAdaptionSetting; + private NewEpisodesAction newEpisodesAction; private String username; private String password; private float feedPlaybackSpeed; @@ -38,15 +75,17 @@ public class FeedPreferences implements Serializable { private final Set<String> tags = new HashSet<>(); public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction autoDeleteAction, - VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) { - this(feedID, autoDownload, true, autoDeleteAction, volumeAdaptionSetting, - username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, new HashSet<>()); + VolumeAdaptionSetting volumeAdaptionSetting, NewEpisodesAction newEpisodesAction, + String username, String password) { + this(feedID, autoDownload, true, autoDeleteAction, volumeAdaptionSetting, username, password, + new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, newEpisodesAction, new HashSet<>()); } public FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction autoDeleteAction, VolumeAdaptionSetting volumeAdaptionSetting, - String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed, - int feedSkipIntro, int feedSkipEnding, boolean showEpisodeNotification, + String username, String password, @NonNull FeedFilter filter, + float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding, + boolean showEpisodeNotification, NewEpisodesAction newEpisodesAction, Set<String> tags) { this.feedID = feedID; this.autoDownload = autoDownload; @@ -60,6 +99,7 @@ public class FeedPreferences implements Serializable { this.feedSkipIntro = feedSkipIntro; this.feedSkipEnding = feedSkipEnding; this.showEpisodeNotification = showEpisodeNotification; + this.newEpisodesAction = newEpisodesAction; this.tags.addAll(tags); } @@ -140,6 +180,10 @@ public class FeedPreferences implements Serializable { return volumeAdaptionSetting; } + public NewEpisodesAction getNewEpisodesAction() { + return newEpisodesAction; + } + public void setAutoDeleteAction(AutoDeleteAction autoDeleteAction) { this.autoDeleteAction = autoDeleteAction; } @@ -148,6 +192,10 @@ public class FeedPreferences implements Serializable { this.volumeAdaptionSetting = volumeAdaptionSetting; } + public void setNewEpisodesAction(NewEpisodesAction newEpisodesAction) { + this.newEpisodesAction = newEpisodesAction; + } + public AutoDeleteAction getCurrentAutoDelete() { return autoDeleteAction; } diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java index d4674c79d..30be87931 100644 --- a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java @@ -48,6 +48,6 @@ public class FyydPodcastSearcher implements PodcastSearcher { @Override public String getName() { - return "Fyyd"; + return "fyyd"; } } diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java index b2ac1766c..32e150f76 100644 --- a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java @@ -111,6 +111,6 @@ public class ItunesPodcastSearcher implements PodcastSearcher { @Override public String getName() { - return "iTunes"; + return "Apple"; } } diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java index 53ea00235..5fa48458e 100644 --- a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java @@ -2,12 +2,8 @@ package de.danoeh.antennapod.net.discovery; import android.content.Context; import android.util.Log; - import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import io.reactivex.Single; -import io.reactivex.SingleOnSubscribe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; +import de.danoeh.antennapod.model.feed.Feed; import okhttp3.CacheControl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -18,8 +14,10 @@ import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.concurrent.TimeUnit; public class ItunesTopListLoader { @@ -30,39 +28,54 @@ public class ItunesTopListLoader { public static final String PREF_KEY_NEEDS_CONFIRM = "needs_confirm"; public static final String PREFS = "CountryRegionPrefs"; public static final String COUNTRY_CODE_UNSET = "99"; + private static final int NUM_LOADED = 25; public ItunesTopListLoader(Context context) { this.context = context; } - public Single<List<PodcastSearchResult>> loadToplist(String country, int limit) { - return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> { - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - String feedString; - String loadCountry = country; + public List<PodcastSearchResult> loadToplist(String country, int limit, List<Feed> subscribed) + throws JSONException, IOException { + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + String feedString; + String loadCountry = country; + if (COUNTRY_CODE_UNSET.equals(country)) { + loadCountry = Locale.getDefault().getCountry(); + } + try { + feedString = getTopListFeed(client, loadCountry); + } catch (IOException e) { if (COUNTRY_CODE_UNSET.equals(country)) { - loadCountry = Locale.getDefault().getCountry(); - } - try { - feedString = getTopListFeed(client, loadCountry, limit); - } catch (IOException e) { - if (COUNTRY_CODE_UNSET.equals(country)) { - feedString = getTopListFeed(client, "US", limit); - } else { - emitter.onError(e); - return; - } + feedString = getTopListFeed(client, "US"); + } else { + throw e; } + } + return removeSubscribed(parseFeed(feedString), subscribed, limit); + } - List<PodcastSearchResult> podcasts = parseFeed(feedString); - emitter.onSuccess(podcasts); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + private static List<PodcastSearchResult> removeSubscribed( + List<PodcastSearchResult> suggestedPodcasts, List<Feed> subscribedFeeds, int limit) { + Set<String> subscribedPodcastsSet = new HashSet<>(); + for (Feed subscribedFeed : subscribedFeeds) { + if (subscribedFeed.getTitle() != null && subscribedFeed.getAuthor() != null) { + subscribedPodcastsSet.add(subscribedFeed.getTitle().trim() + " - " + subscribedFeed.getAuthor().trim()); + } + } + List<PodcastSearchResult> suggestedNotSubscribed = new ArrayList<>(); + for (PodcastSearchResult suggested : suggestedPodcasts) { + if (!subscribedPodcastsSet.contains(suggested.title.trim())) { + suggestedNotSubscribed.add(suggested); + } + if (suggestedNotSubscribed.size() == limit) { + return suggestedNotSubscribed; + } + } + return suggestedNotSubscribed; } - private String getTopListFeed(OkHttpClient client, String country, int limit) throws IOException { - String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit=" + limit + "/explicit=true/json"; + private String getTopListFeed(OkHttpClient client, String country) throws IOException { + String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit=" + NUM_LOADED + "/explicit=true/json"; Log.d(TAG, "Feed URL " + String.format(url, country)); Request.Builder httpReq = new Request.Builder() .cacheControl(new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()) diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java index 33c5e72f6..105667393 100644 --- a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java @@ -83,7 +83,7 @@ public class PodcastIndexPodcastSearcher implements PodcastSearcher { @Override public String getName() { - return "Podcastindex.org"; + return "Podcast Index"; } private Request buildAuthenticatedRequest(String url) { diff --git a/net/download/service-interface/build.gradle b/net/download/service-interface/build.gradle index 785326bab..5779ab51d 100644 --- a/net/download/service-interface/build.gradle +++ b/net/download/service-interface/build.gradle @@ -5,7 +5,7 @@ plugins { apply from: "../../../common.gradle" android { - lintOptions { + lint { disable 'ParcelClassLoader' } } diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java index e5c6662eb..9f9737edc 100644 --- a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java @@ -263,7 +263,7 @@ public class DownloadRequest implements Parcelable { public static class Builder { private final String destination; - private final String source; + private String source; private final String title; private String username; private String password; @@ -296,6 +296,10 @@ public class DownloadRequest implements Parcelable { return this; } + public void setSource(String source) { + this.source = source; + } + public void setForce(boolean force) { if (force) { lastModified = null; diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java index 54987a83e..b5d0cd991 100644 --- a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.net.download.serviceinterface; import android.content.Context; +import android.content.Intent; public abstract class DownloadServiceInterface { private static DownloadServiceInterface impl; @@ -15,6 +16,8 @@ public abstract class DownloadServiceInterface { public abstract void download(Context context, boolean cleanupMedia, DownloadRequest... requests); + public abstract Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests); + public abstract void refreshAllFeeds(Context context, boolean initiatedByUser); public abstract void cancel(Context context, String url); diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java index 251c59c61..947746485 100644 --- a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java @@ -1,12 +1,17 @@ package de.danoeh.antennapod.net.download.serviceinterface; import android.content.Context; +import android.content.Intent; public class DownloadServiceInterfaceStub extends DownloadServiceInterface { public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { } + public Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests) { + return null; + } + public void refreshAllFeeds(Context context, boolean initiatedByUser) { } diff --git a/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java index 16b2f0931..252950a99 100644 --- a/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.net.ssl; +import android.annotation.SuppressLint; + import javax.net.ssl.X509TrustManager; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -12,6 +14,7 @@ import java.util.List; * trusts a certificate chain, then it is trusted by the composite manager. * Based on https://stackoverflow.com/a/16229909 */ +@SuppressLint("CustomX509TrustManager") public class CompositeX509TrustManager implements X509TrustManager { private final List<X509TrustManager> trustManagers; diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java index 6b364fa73..dd9cb836b 100644 --- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java @@ -31,6 +31,6 @@ public class FeedHandler { saxParser.parse(inputSource, handler); inputStreamReader.close(); - return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); + return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls, handler.state.redirectUrl); } } diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java index 43b3387a0..6db3ca47f 100644 --- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java @@ -11,9 +11,11 @@ public class FeedHandlerResult { public final Feed feed; public final Map<String, String> alternateFeedUrls; + public final String redirectUrl; - public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls) { + public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls, String redirectUrl) { this.feed = feed; this.alternateFeedUrls = alternateFeedUrls; + this.redirectUrl = redirectUrl; } } diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java index 706a328e8..650931fc1 100644 --- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java @@ -26,6 +26,7 @@ public class HandlerState { * URL of the feed, the value is the title */ public final Map<String, String> alternateUrls; + public String redirectUrl = null; private final ArrayList<FeedItem> items; private FeedItem currentItem; private FeedFunding currentFunding; diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java index 9c57d4ef3..cb4bcb8a7 100644 --- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java @@ -23,6 +23,7 @@ public class Itunes extends Namespace { public static final String DURATION = "duration"; private static final String SUBTITLE = "subtitle"; private static final String SUMMARY = "summary"; + private static final String NEW_FEED_URL = "new-feed-url"; @Override public SyndElement handleElementStart(String localName, HandlerState state, @@ -76,6 +77,8 @@ public class Itunes extends Namespace { } else if (Rss20.CHANNEL.equals(state.getSecondTag().getName()) && state.getFeed() != null) { state.getFeed().setDescription(content); } + } else if (NEW_FEED_URL.equals(localName) && content.trim().startsWith("http")) { + state.redirectUrl = content.trim(); } } } diff --git a/playFlavor.gradle b/playFlavor.gradle index 7289bd4c9..9f2a3d8d5 100644 --- a/playFlavor.gradle +++ b/playFlavor.gradle @@ -1,5 +1,5 @@ android { - flavorDimensions "market" + flavorDimensions += ["market"] productFlavors { free { dimension "market" diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java index a5b6df41e..ef15cef1d 100644 --- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java +++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java @@ -9,7 +9,6 @@ import android.util.Pair; import android.view.SurfaceHolder; import java.util.List; -import java.util.concurrent.Future; import androidx.annotation.Nullable; import de.danoeh.antennapod.model.playback.MediaType; @@ -158,13 +157,6 @@ public abstract class PlaybackServiceMediaPlayer { */ public abstract void setVolume(float volumeLeft, float volumeRight); - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - public abstract boolean canDownmix(); - - public abstract void setDownmix(boolean enable); - public abstract MediaType getCurrentMediaType(); public abstract boolean isStreaming(); @@ -232,8 +224,8 @@ public abstract class PlaybackServiceMediaPlayer { * * @see #endPlayback(boolean, boolean, boolean, boolean) */ - public Future<?> stopPlayback(boolean toStoppedState) { - return endPlayback(false, false, false, toStoppedState); + public void stopPlayback(boolean toStoppedState) { + endPlayback(false, false, false, toStoppedState); } /** @@ -262,7 +254,7 @@ public abstract class PlaybackServiceMediaPlayer { * * @return a Future, just for the purpose of tracking its execution. */ - protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, + protected abstract void endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, boolean toStoppedState); /** diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java index cdea61c39..e91baa985 100644 --- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.playback.cast; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; import com.google.android.gms.cast.framework.CastOptions; @@ -9,6 +10,7 @@ import com.google.android.gms.cast.framework.SessionProvider; import java.util.List; @SuppressWarnings("unused") +@SuppressLint("VisibleForTests") public class CastOptionsProvider implements OptionsProvider { @Override @NonNull diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java index c4f135157..8f0738e53 100644 --- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.playback.cast; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; import android.util.Log; @@ -8,8 +9,6 @@ import android.view.SurfaceHolder; import java.util.Collections; import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; import androidx.annotation.Nullable; @@ -38,6 +37,7 @@ import org.greenrobot.eventbus.EventBus; /** * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices. */ +@SuppressLint("VisibleForTests") public class CastPsmp extends PlaybackServiceMediaPlayer { public static final String TAG = "CastPSMP"; @@ -434,16 +434,6 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { } @Override - public boolean canDownmix() { - return false; - } - - @Override - public void setDownmix(boolean enable) { - throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player"); - } - - @Override public MediaType getCurrentMediaType() { return mediaType; } @@ -500,7 +490,7 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { } @Override - protected Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, + protected void endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, boolean toStoppedState) { Log.d(TAG, "endPlayback() called"); boolean isPlaying = playerStatus == PlayerStatus.PLAYING; @@ -547,10 +537,6 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { callback.onPlaybackPause(currentMedia, currentMedia != null ? currentMedia.getPosition() : Playable.INVALID_TIME); } - - FutureTask<?> future = new FutureTask<>(() -> { }, null); - future.run(); - return future; } @Override diff --git a/createContributors.py b/scripts/createContributors.py index fba19c793..fba19c793 100644 --- a/createContributors.py +++ b/scripts/createContributors.py diff --git a/scripts/getChangelog.py b/scripts/getChangelog.py new file mode 100644 index 000000000..cd02f3383 --- /dev/null +++ b/scripts/getChangelog.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import requests +import time +import re + +REPO = "AntennaPod/AntennaPod" + +print("Hello, welcome to the AntennaPod PR list generator!") +print("First, please enter your GitHub API token.") +print("If you don't have one yet, create it at https://github.com/settings/tokens") + +def get_token(): + TOKEN = "" + while not TOKEN: + TOKEN = input('Token: ').strip() + return TOKEN + +TOKEN = get_token() +print("Grand, thank you! (" + TOKEN + " is noted)") + +print() +print("Now, what do you want to compare?") +print("Please enter a release code or branch") +print("[default: latest GitHub release]") +BASE = input('Base: ') +if BASE == "": + response = requests.get("https://api.github.com/repos/" + REPO + "/releases/latest", headers={'Authorization': 'token ' + TOKEN}) + while response.status_code == 401: + print("Error: Invalid GitHub API token.") + TOKEN = get_token() + response = requests.get("https://api.github.com/repos/" + REPO + "/releases/latest", headers={'Authorization': 'token ' + TOKEN}) + release = response.json() + BASE = release["tag_name"] + print("Okido, latest release (" + BASE + ") it is!") +else: + print("Noted") + +print() +print("Then, what should be our endpoint?") +print("[default: 'master']") +HEAD = input('Head: ') +if HEAD == "": + print("Righty, master it is!") + HEAD="master" +else: + print("Roger that.") + +def print_seen(): + print(" [already seen] " + pr_details["title"] + " (#" + str(pr_details["number"]) + ")") + +print() +prsSeen = set() +filename = BASE + " - " + HEAD + " changelog.csv" +outputFile = open(filename, 'w') +outputFile.write("Type,Merge date,URL,Title,Author,Type,Functionality group\n") +commits = requests.get("https://api.github.com/repos/" + REPO + "/compare/" + BASE + "..." + HEAD, headers={'Authorization': 'token ' + TOKEN}).json() +numCommits = len(commits["commits"]) +for i in range(numCommits): + sha = commits["commits"][i]["sha"] + commit = commits["commits"][i] + print("Commit "+ str(i+1) + " of " + str(numCommits)) + if "Merge pull request #" in commit["commit"]["message"] or "Merge branch" in commit["commit"]["message"]: + print(" [is merge commit]") + continue + pr_match = re.search(r'\(#(\d{4})\)', commit["commit"]["message"]) + if pr_match: + pr_number = pr_match.group(1) + if pr_number in prsSeen: + print_seen() + continue + pr_details = requests.get("https://api.github.com/repos/" + REPO + "/pulls/" + pr_number, headers={'Authorization': 'token ' + TOKEN}).json() + outputFile.write("PR," + pr_details["merged_at"] + "," + pr_details["html_url"] + ",\"" + pr_details["title"] + "\"," + pr_details["user"]["login"] + "\n") + print(" " + pr_details["title"] + " (#" + str(pr_details["number"]) + ")") + prsSeen.add(pr_number) + continue + time.sleep(1.5) # Avoid rate limit + prs = requests.get("https://api.github.com/search/issues?q=repo:" + REPO + "+type:pr+is:merged+" + sha, headers={'Authorization': 'token ' + TOKEN}).json() + if len(prs["items"]) == 0: + outputFile.write("Commit," + commit["commit"]["committer"]["date"] + "," + commit["html_url"] + ",\"" + commit["commit"]["message"].splitlines()[0] + "\"," + commit["committer"]["login"] + "\n") + print(" [orphan] " + commit["commit"]["message"].splitlines()[0]) + continue + pr_details = prs["items"][0] + if pr_details["number"] in prsSeen: + print_seen() + continue + outputFile.write("PR," + pr_details["pull_request"]["merged_at"] + "," + pr_details["html_url"] + ",\"" + pr_details["title"] + "\"," + pr_details["user"]["login"] + "\n") + print(" " + pr_details["title"] + " (#" + str(pr_details["number"]) + ")") + prsSeen.add(pr_details["number"]) +outputFile.close() diff --git a/makeRelease.sh b/scripts/makeRelease.sh index 34c79f319..34c79f319 100755..100644 --- a/makeRelease.sh +++ b/scripts/makeRelease.sh diff --git a/storage/database/build.gradle b/storage/database/build.gradle index 141cdb086..0dc5cef63 100644 --- a/storage/database/build.gradle +++ b/storage/database/build.gradle @@ -4,8 +4,8 @@ plugins { apply from: "../../common.gradle" android { - lintOptions { - disable "StaticFieldLeak" + lint { + disable "StaticFieldLeak", "StringFormatCount", "StringFormatMatches", "StringFormatInvalid", "PluralsCandidate", "StringFormatTrivial" } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java index 78eaf6964..e4a1f5a3d 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java @@ -330,6 +330,10 @@ class DBUpgrader { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " ADD COLUMN " + PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL + " TEXT"); } + if (oldVersion < 3010000) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_NEW_EPISODES_ACTION + " INTEGER DEFAULT 0"); + } } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java index 35064b605..40933e8d1 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java @@ -38,6 +38,8 @@ import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.storage.database.mapper.FeedItemFilterQuery; +import de.danoeh.antennapod.storage.database.mapper.FeedItemSortQuery; + import org.apache.commons.io.FileUtils; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; @@ -50,7 +52,7 @@ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; public static final String DATABASE_NAME = "Antennapod.db"; - public static final int VERSION = 2060000; + public static final int VERSION = 3010000; /** * Maximum number of arguments for IN-operator. @@ -116,6 +118,7 @@ public class PodDBAdapter { public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; public static final String KEY_FEED_TAGS = "tags"; public static final String KEY_EPISODE_NOTIFICATION = "episode_notification"; + public static final String KEY_NEW_EPISODES_ACTION = "new_episodes_action"; public static final String KEY_PODCASTINDEX_CHAPTER_URL = "podcastindex_chapter_url"; // Table names @@ -157,7 +160,8 @@ public class PodDBAdapter { + KEY_FEED_TAGS + " TEXT," + KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0," + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0," - + KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0)"; + + KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0," + + KEY_NEW_EPISODES_ACTION + " INTEGER DEFAULT 0)"; private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY @@ -307,7 +311,8 @@ public class PodDBAdapter { + TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS + ", " + TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO + ", " + TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING + ", " - + TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION; + + TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION + ", " + + TABLE_NAME_FEEDS + "." + KEY_NEW_EPISODES_ACTION; private static final String JOIN_FEED_ITEM_AND_MEDIA = " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " "; @@ -444,7 +449,7 @@ public class PodDBAdapter { ContentValues values = new ContentValues(); values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.getAutoDownload()); values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated()); - values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().ordinal()); + values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().code); values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger()); values.put(KEY_USERNAME, prefs.getUsername()); values.put(KEY_PASSWORD, prefs.getPassword()); @@ -456,6 +461,7 @@ public class PodDBAdapter { values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro()); values.put(KEY_FEED_SKIP_ENDING, prefs.getFeedSkipEnding()); values.put(KEY_EPISODE_NOTIFICATION, prefs.getShowEpisodeNotification()); + values.put(KEY_NEW_EPISODES_ACTION, prefs.getNewEpisodesAction().code); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } @@ -728,6 +734,13 @@ public class PodDBAdapter { } } + public void resetPagedFeedPage(Feed feed) { + final String sql = "UPDATE " + TABLE_NAME_FEEDS + + " SET " + KEY_NEXT_PAGE_LINK + "=" + KEY_DOWNLOAD_URL + + " WHERE " + KEY_ID + "=" + feed.getId(); + db.execSQL(sql); + } + public void setFeedLastUpdateFailed(long feedId, boolean failed) { final String sql = "UPDATE " + TABLE_NAME_FEEDS + " SET " + KEY_LAST_UPDATE_FAILED + "=" + (failed ? "1" : "0") @@ -1041,27 +1054,20 @@ public class PodDBAdapter { db.execSQL(sql); } - /** - * Returns a cursor which contains all feed items that are considered new. - * Excludes those feeds that do not have 'Keep Updated' enabled. - * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. - */ - public final Cursor getNewItemsCursor(int offset, int limit) { - final String query = SELECT_FEED_ITEMS_AND_MEDIA - + " INNER JOIN " + TABLE_NAME_FEEDS - + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID - + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW - + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0" - + " ORDER BY " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC" - + " LIMIT " + offset + ", " + limit; + public final Cursor getEpisodesCursor(int offset, int limit, FeedItemFilter filter, SortOrder sortOrder) { + String orderByQuery = FeedItemSortQuery.generateFrom(sortOrder); + String filterQuery = FeedItemFilterQuery.generateFrom(filter); + String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery; + final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause + + "ORDER BY " + orderByQuery + " LIMIT " + offset + ", " + limit; return db.rawQuery(query, null); } - public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit, FeedItemFilter filter) { + public final Cursor getEpisodeCountCursor(FeedItemFilter filter) { String filterQuery = FeedItemFilterQuery.generateFrom(filter); String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery; - final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause - + " ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit; + final String query = "SELECT count(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") FROM " + TABLE_NAME_FEED_ITEMS + + JOIN_FEED_ITEM_AND_MEDIA + whereClause; return db.rawQuery(query, null); } @@ -1087,26 +1093,6 @@ public class PodDBAdapter { return "((" + SELECT_KEY_ITEM_ID + " * " + seed + ") % 46471)"; } - public final Cursor getTotalEpisodeCountCursor(FeedItemFilter filter) { - String filterQuery = FeedItemFilterQuery.generateFrom(filter); - String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery; - final String query = "SELECT count(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") FROM " + TABLE_NAME_FEED_ITEMS - + JOIN_FEED_ITEM_AND_MEDIA + whereClause; - return db.rawQuery(query, null); - } - - public Cursor getDownloadedItemsCursor() { - final String query = SELECT_FEED_ITEMS_AND_MEDIA - + "WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " > 0"; - return db.rawQuery(query, null); - } - - public Cursor getPlayedItemsCursor() { - final String query = SELECT_FEED_ITEMS_AND_MEDIA - + "WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.PLAYED; - return db.rawQuery(query, null); - } - /** * Returns a cursor which contains feed media objects with a playback * completion date in ascending order. @@ -1214,7 +1200,7 @@ public class PodDBAdapter { + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + "> 0"; } final String timeFilter = lastPlayedTime + ">=" + timeFilterFrom - + " AND " + lastPlayedTime + "<=" + timeFilterTo; + + " AND " + lastPlayedTime + "<" + timeFilterTo; String playedTime = TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYED_DURATION; if (includeMarkedAsPlayed) { playedTime = "(CASE WHEN " + playedTime + " != 0" @@ -1255,25 +1241,6 @@ public class PodDBAdapter { return result; } - public final int getNumberOfNewItems() { - Object[] args = new String[]{ - TABLE_NAME_FEED_ITEMS + "." + KEY_ID, - TABLE_NAME_FEED_ITEMS, - TABLE_NAME_FEEDS, - TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID, - TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW - + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0" - }; - final String query = String.format("SELECT COUNT(%s) FROM %s INNER JOIN %s ON %s WHERE %s", args); - Cursor c = db.rawQuery(query, null); - int result = 0; - if (c.moveToFirst()) { - result = c.getInt(0); - } - c.close(); - return result; - } - public final Map<Long, Integer> getFeedCounters(FeedCounter setting, long... feedIds) { String whereRead; switch (setting) { @@ -1359,19 +1326,6 @@ public class PodDBAdapter { return result; } - public final int getNumberOfDownloadedEpisodes() { - final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + - " WHERE " + KEY_DOWNLOADED + " > 0"; - - Cursor c = db.rawQuery(query, null); - int result = 0; - if (c.moveToFirst()) { - result = c.getInt(0); - } - c.close(); - return result; - } - /** * Uses DatabaseUtils to escape a search query and removes ' at the * beginning and the end of the string returned by the escape method. @@ -1530,6 +1484,9 @@ public class PodDBAdapter { public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + newVersion + "."); DBUpgrader.upgrade(db, oldVersion, newVersion); + + db.execSQL("DELETE FROM " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + " WHERE " + + PodDBAdapter.KEY_COMPLETION_DATE + "<" + (System.currentTimeMillis() - 7L * 24L * 3600L * 1000L)); } } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemSortQuery.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemSortQuery.java new file mode 100644 index 000000000..aae5154d3 --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemSortQuery.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.storage.database.mapper; + +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.database.PodDBAdapter; + +public class FeedItemSortQuery { + public static String generateFrom(SortOrder sortOrder) { + String sortQuery = ""; + switch (sortOrder) { + case EPISODE_TITLE_A_Z: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_TITLE + " " + "ASC"; + break; + case EPISODE_TITLE_Z_A: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_TITLE + " " + "DESC"; + break; + case DATE_OLD_NEW: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_PUBDATE + " " + "ASC"; + break; + case DATE_NEW_OLD: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_PUBDATE + " " + "DESC"; + break; + case DURATION_SHORT_LONG: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DURATION + " " + "ASC"; + break; + case DURATION_LONG_SHORT: + sortQuery = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DURATION + " " + "DESC"; + break; + default: + sortQuery = ""; + break; + } + return sortQuery; + } +}
\ No newline at end of file diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java index 289bcbab8..2de100dff 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java @@ -34,14 +34,14 @@ public abstract class FeedPreferencesCursorMapper { int indexAutoSkipIntro = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_SKIP_INTRO); int indexAutoSkipEnding = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_SKIP_ENDING); int indexEpisodeNotification = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_EPISODE_NOTIFICATION); + int indexNewEpisodesAction = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_NEW_EPISODES_ACTION); int indexTags = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_TAGS); long feedId = cursor.getLong(indexId); boolean autoDownload = cursor.getInt(indexAutoDownload) > 0; boolean autoRefresh = cursor.getInt(indexAutoRefresh) > 0; - int autoDeleteActionIndex = cursor.getInt(indexAutoDeleteAction); FeedPreferences.AutoDeleteAction autoDeleteAction = - FeedPreferences.AutoDeleteAction.values()[autoDeleteActionIndex]; + FeedPreferences.AutoDeleteAction.fromCode(cursor.getInt(indexAutoDeleteAction)); int volumeAdaptionValue = cursor.getInt(indexVolumeAdaption); VolumeAdaptionSetting volumeAdaptionSetting = VolumeAdaptionSetting.fromInteger(volumeAdaptionValue); String username = cursor.getString(indexUsername); @@ -52,6 +52,8 @@ public abstract class FeedPreferencesCursorMapper { float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed); int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro); int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding); + FeedPreferences.NewEpisodesAction feedNewEpisodesAction = + FeedPreferences.NewEpisodesAction.fromCode(cursor.getInt(indexNewEpisodesAction)); boolean showNotification = cursor.getInt(indexEpisodeNotification) > 0; String tagsString = cursor.getString(indexTags); if (TextUtils.isEmpty(tagsString)) { @@ -69,6 +71,7 @@ public abstract class FeedPreferencesCursorMapper { feedAutoSkipIntro, feedAutoSkipEnding, showNotification, + feedNewEpisodesAction, new HashSet<>(Arrays.asList(tagsString.split(FeedPreferences.TAG_SEPARATOR)))); } } diff --git a/storage/preferences/build.gradle b/storage/preferences/build.gradle index 0c852bf7a..0da810e28 100644 --- a/storage/preferences/build.gradle +++ b/storage/preferences/build.gradle @@ -4,7 +4,7 @@ plugins { apply from: "../../common.gradle" android { - lintOptions { + lint { disable "StaticFieldLeak" } } diff --git a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java index 14fb94018..8a19300bb 100644 --- a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java +++ b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java @@ -6,13 +6,17 @@ import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; - +import de.danoeh.antennapod.model.download.ProxyConfig; +import de.danoeh.antennapod.model.feed.FeedCounter; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.model.feed.SubscriptionsFilter; +import de.danoeh.antennapod.model.playback.MediaType; import org.json.JSONArray; import org.json.JSONException; @@ -27,13 +31,6 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.model.feed.FeedCounter; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.feed.SubscriptionsFilter; -import de.danoeh.antennapod.model.download.ProxyConfig; -import de.danoeh.antennapod.model.feed.SortOrder; /** * Provides access to preferences set by the user in the settings screen. A @@ -48,6 +45,7 @@ public class UserPreferences { // User Interface public static final String PREF_THEME = "prefTheme"; + public static final String PREF_THEME_BLACK = "prefThemeBlack"; public static final String PREF_TINTED_COLORS = "prefTintedColors"; public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; public static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; @@ -57,7 +55,6 @@ public class UserPreferences { public static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; public static final String PREF_COMPACT_NOTIFICATION_BUTTONS = "prefCompactNotificationButtons"; - public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground"; private static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"; private static final String PREF_SHOW_AUTO_DOWNLOAD_REPORT = "prefShowAutoDownloadReport"; public static final String PREF_DEFAULT_PAGE = "prefDefaultPage"; @@ -67,6 +64,9 @@ public class UserPreferences { public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted"; public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder"; + public static final String PREF_NEW_EPISODES_ACTION = "prefNewEpisodesAction"; + private static final String PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"; + private static final String PREF_INBOX_SORTED_ORDER = "prefInboxSortedOrder"; // Playback public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; @@ -108,11 +108,8 @@ public class UserPreferences { // Other private static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_DELETE_REMOVES_FROM_QUEUE = "prefDeleteRemovesFromQueue"; - public static final String PREF_USAGE_COUNTING_DATE = "prefUsageCounting"; // Mediaplayer - public static final String PREF_MEDIA_PLAYER = "prefMediaPlayer"; - public static final String PREF_MEDIA_PLAYER_EXOPLAYER = "exoplayer"; private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; private static final String PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed"; public static final String PREF_PLAYBACK_SKIP_SILENCE = "prefSkipSilence"; @@ -121,7 +118,6 @@ public class UserPreferences { private static final String PREF_QUEUE_LOCKED = "prefQueueLocked"; // Experimental - private static final String PREF_STEREO_TO_MONO = "PrefStereoToMono"; public static final int EPISODE_CLEANUP_QUEUE = -1; public static final int EPISODE_CLEANUP_NULL = -2; public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3; @@ -158,19 +154,35 @@ public class UserPreferences { LIGHT, DARK, BLACK, SYSTEM } + public static void setTheme(ThemePreference theme) { + switch (theme) { + case LIGHT: + prefs.edit().putString(PREF_THEME, "0").apply(); + break; + case DARK: + prefs.edit().putString(PREF_THEME, "1").apply(); + break; + default: + prefs.edit().putString(PREF_THEME, "system").apply(); + break; + } + } + public static ThemePreference getTheme() { switch (prefs.getString(PREF_THEME, "system")) { case "0": return ThemePreference.LIGHT; case "1": return ThemePreference.DARK; - case "2": - return ThemePreference.BLACK; default: return ThemePreference.SYSTEM; } } + public static boolean getIsBlackTheme() { + return prefs.getBoolean(PREF_THEME_BLACK, false); + } + public static boolean getIsThemeColorTinted() { return Build.VERSION.SDK_INT >= 31 && prefs.getBoolean(PREF_TINTED_COLORS, false); } @@ -279,15 +291,6 @@ public class UserPreferences { } /** - * Returns true if the lockscreen background should be set to the current episode's image - * - * @return {@code true} if the lockscreen background should be set, {@code false} otherwise - */ - public static boolean setLockscreenBackground() { - return prefs.getBoolean(PREF_LOCKSCREEN_BACKGROUND, true); - } - - /** * Returns true if download reports are shown * * @return {@code true} if download reports are shown, {@code false} otherwise @@ -332,7 +335,7 @@ public class UserPreferences { } public enum EnqueueLocation { - BACK, FRONT, AFTER_CURRENTLY_PLAYING + BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM } @NonNull @@ -446,34 +449,12 @@ public class UserPreferences { return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true); } - - /* - * Returns update interval in milliseconds; value 0 means that auto update is disabled - * or feeds are updated at a certain time of day - */ public static long getUpdateInterval() { - String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0"); - if(!updateInterval.contains(":")) { - return readUpdateInterval(updateInterval); - } else { - return 0; - } - } - - public static int[] getUpdateTimeOfDay() { - String datetime = prefs.getString(PREF_UPDATE_INTERVAL, ""); - if(datetime.length() >= 3 && datetime.contains(":")) { - String[] parts = datetime.split(":"); - int hourOfDay = Integer.parseInt(parts[0]); - int minute = Integer.parseInt(parts[1]); - return new int[] { hourOfDay, minute }; - } else { - return new int[0]; - } + return Integer.parseInt(prefs.getString(PREF_UPDATE_INTERVAL, "12")); } public static boolean isAutoUpdateDisabled() { - return prefs.getString(PREF_UPDATE_INTERVAL, "").equals("0"); + return getUpdateInterval() == 0; } private static boolean isAllowMobileFor(String type) { @@ -487,6 +468,10 @@ public class UserPreferences { return isAllowMobileFor("feed_refresh"); } + public static boolean isAllowMobileSync() { + return isAllowMobileFor("sync"); + } + public static boolean isAllowMobileEpisodeDownload() { return isAllowMobileFor("episode_download"); } @@ -536,6 +521,10 @@ public class UserPreferences { setAllowMobileFor("images", allow); } + public static void setAllowMobileSync(boolean allow) { + setAllowMobileFor("sync", allow); + } + public static int getParallelDownloads() { return Integer.parseInt(prefs.getString(PREF_PARALLEL_DOWNLOADS, "4")); } @@ -667,24 +656,6 @@ public class UserPreferences { .apply(); } - public static void setUpdateInterval(long hours) { - prefs.edit() - .putString(PREF_UPDATE_INTERVAL, String.valueOf(hours)) - .apply(); - } - - public static void setUpdateTimeOfDay(int hourOfDay, int minute) { - prefs.edit() - .putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute) - .apply(); - } - - public static void disableAutoUpdate() { - prefs.edit() - .putString(PREF_UPDATE_INTERVAL, "0") - .apply(); - } - public static boolean gpodnetNotificationsEnabled() { if (Build.VERSION.SDK_INT >= 26) { return true; // System handles notification preferences @@ -725,11 +696,6 @@ public class UserPreferences { .apply(); } - private static long readUpdateInterval(String valueFromPrefs) { - int hours = Integer.parseInt(valueFromPrefs); - return TimeUnit.HOURS.toMillis(hours); - } - private static List<Float> readPlaybackSpeedArray(String valueFromPrefs) { if (valueFromPrefs != null) { try { @@ -748,32 +714,6 @@ public class UserPreferences { return Arrays.asList(1.0f, 1.25f, 1.5f); } - public static String getMediaPlayer() { - return prefs.getString(PREF_MEDIA_PLAYER, PREF_MEDIA_PLAYER_EXOPLAYER); - } - - public static boolean useSonic() { - return getMediaPlayer().equals("sonic"); - } - - public static boolean useExoplayer() { - return getMediaPlayer().equals(PREF_MEDIA_PLAYER_EXOPLAYER); - } - - public static void enableExoplayer() { - prefs.edit().putString(PREF_MEDIA_PLAYER, PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - } - - public static boolean stereoToMono() { - return prefs.getBoolean(PREF_STEREO_TO_MONO, false); - } - - public static void stereoToMono(boolean enable) { - prefs.edit() - .putBoolean(PREF_STEREO_TO_MONO, enable) - .apply(); - } - public static int getEpisodeCleanupValue() { return Integer.parseInt(prefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)); } @@ -848,15 +788,6 @@ public class UserPreferences { } } - /** - * - * @return true if auto update is set to a specific time - * false if auto update is set to interval - */ - public static boolean isAutoUpdateTimeOfDay() { - return getUpdateTimeOfDay().length == 2; - } - public static String getDefaultPage() { return prefs.getString(PREF_DEFAULT_PAGE, "HomeFragment"); } @@ -926,6 +857,36 @@ public class UserPreferences { .apply(); } + public static FeedPreferences.NewEpisodesAction getNewEpisodesAction() { + String str = prefs.getString(PREF_NEW_EPISODES_ACTION, + "" + FeedPreferences.NewEpisodesAction.ADD_TO_INBOX.code); + return FeedPreferences.NewEpisodesAction.fromCode(Integer.parseInt(str)); + } + + /** + * Returns the sort order for the downloads. + */ + public static SortOrder getDownloadsSortedOrder() { + String sortOrderStr = prefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + SortOrder.DATE_NEW_OLD.code); + return SortOrder.fromCodeString(sortOrderStr); + } + + /** + * Sets the sort order for the downloads. + */ + public static void setDownloadsSortedOrder(SortOrder sortOrder) { + prefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder.code).apply(); + } + + public static SortOrder getInboxSortedOrder() { + String sortOrderStr = prefs.getString(PREF_INBOX_SORTED_ORDER, "" + SortOrder.DATE_NEW_OLD.code); + return SortOrder.fromCodeString(sortOrderStr); + } + + public static void setInboxSortedOrder(SortOrder sortOrder) { + prefs.edit().putString(PREF_INBOX_SORTED_ORDER, "" + sortOrder.code).apply(); + } + public static SubscriptionsFilter getSubscriptionsFilter() { String value = prefs.getString(PREF_FILTER_FEED, ""); return new SubscriptionsFilter(value); diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java new file mode 100644 index 000000000..03c5e915e --- /dev/null +++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java @@ -0,0 +1,39 @@ +package de.danoeh.antennapod.ui.appstartintent; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Parcelable; + +/** + * Launches the download authentication activity of the app with specific arguments. + * Does not require a dependency on the actual implementation of the activity. + */ +public class DownloadAuthenticationActivityStarter { + public static final String INTENT = "de.danoeh.antennapod.intents.DOWNLOAD_AUTH_ACTIVITY"; + public static final String EXTRA_DOWNLOAD_REQUEST = "download_request"; + + private final Intent intent; + private final Context context; + private final long feedFileId; + + public DownloadAuthenticationActivityStarter(Context context, long feedFileId, Parcelable downloadRequest) { + this.context = context; + this.feedFileId = feedFileId; + intent = new Intent(INTENT); + intent.setAction("request" + feedFileId); + intent.putExtra(EXTRA_DOWNLOAD_REQUEST, downloadRequest); + intent.setPackage(context.getPackageName()); + } + + public Intent getIntent() { + return intent; + } + + public PendingIntent getPendingIntent() { + return PendingIntent.getActivity(context.getApplicationContext(), + ("downloadAuth" + feedFileId).hashCode(), getIntent(), + PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } +} diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java index f91bb9244..1463978ee 100644 --- a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java +++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java @@ -4,6 +4,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Bundle; /** * Launches the main activity of the app with specific arguments. @@ -14,9 +15,13 @@ public class MainActivityStarter { public static final String EXTRA_OPEN_PLAYER = "open_player"; public static final String EXTRA_FEED_ID = "fragment_feed_id"; public static final String EXTRA_ADD_TO_BACK_STACK = "add_to_back_stack"; + public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; + public static final String EXTRA_OPEN_DRAWER = "open_drawer"; + public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; private final Intent intent; private final Context context; + private Bundle fragmentArgs = null; public MainActivityStarter(Context context) { this.context = context; @@ -51,4 +56,22 @@ public class MainActivityStarter { intent.putExtra(EXTRA_ADD_TO_BACK_STACK, true); return this; } + + public MainActivityStarter withFragmentLoaded(String fragmentName) { + intent.putExtra(EXTRA_FRAGMENT_TAG, fragmentName); + return this; + } + + public MainActivityStarter withDrawerOpen() { + intent.putExtra(EXTRA_OPEN_DRAWER, true); + return this; + } + + public MainActivityStarter withFragmentArgs(String name, boolean value) { + if (fragmentArgs == null) { + fragmentArgs = new Bundle(); + } + fragmentArgs.putBoolean(name, value); + return this; + } } diff --git a/ui/app-start-intent/src/main/res/values/pending_intent.xml b/ui/app-start-intent/src/main/res/values/pending_intent.xml index ed7e9b2cd..30b35d926 100644 --- a/ui/app-start-intent/src/main/res/values/pending_intent.xml +++ b/ui/app-start-intent/src/main/res/values/pending_intent.xml @@ -1,6 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <item name="pending_intent_download_service_notification" type="id"/> + <item name="pending_intent_download_service_retry" type="id"/> + <item name="pending_intent_download_cancel_all" type="id"/> <item name="pending_intent_download_service_auth" type="id"/> <item name="pending_intent_download_service_report" type="id"/> <item name="pending_intent_download_service_autodownload_report" type="id"/> diff --git a/ui/common/src/main/res/layout/pager_fragment.xml b/ui/common/src/main/res/layout/pager_fragment.xml index 3987b871d..e479259d5 100644 --- a/ui/common/src/main/res/layout/pager_fragment.xml +++ b/ui/common/src/main/res/layout/pager_fragment.xml @@ -26,7 +26,7 @@ android:layout_height="wrap_content" android:background="?android:attr/windowBackground" app:tabBackground="?attr/selectableItemBackground" - app:tabMode="fixed" + app:tabMode="auto" app:tabGravity="fill" /> <androidx.viewpager2.widget.ViewPager2 diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java index d233fba14..132ed4901 100644 --- a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java +++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.ui.glide; import android.annotation.SuppressLint; import android.content.Context; -import android.graphics.Bitmap; import android.util.Log; import androidx.annotation.NonNull; @@ -51,6 +50,5 @@ public class ApGlideModule extends AppGlideModule { registry.append(String.class, InputStream.class, new NoHttpStringLoader.StreamFactory()); registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory()); - registry.register(Bitmap.class, PaletteBitmap.class, new PaletteBitmapTranscoder()); } } diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java deleted file mode 100644 index a3b590ba2..000000000 --- a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example - */ - -package de.danoeh.antennapod.ui.glide; - -import android.graphics.Bitmap; - -import androidx.annotation.NonNull; -import androidx.palette.graphics.Palette; - -public class PaletteBitmap { - public final Palette palette; - public final Bitmap bitmap; - - public PaletteBitmap(@NonNull Bitmap bitmap, Palette palette) { - this.bitmap = bitmap; - this.palette = palette; - } -}
\ No newline at end of file diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java deleted file mode 100644 index 2fd18a0cb..000000000 --- a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example - */ - -package de.danoeh.antennapod.ui.glide; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.load.engine.Resource; -import com.bumptech.glide.util.Util; - -public class PaletteBitmapResource implements Resource<PaletteBitmap> { - private final PaletteBitmap paletteBitmap; - - public PaletteBitmapResource(@NonNull PaletteBitmap paletteBitmap) { - this.paletteBitmap = paletteBitmap; - } - - @NonNull - @Override - public Class<PaletteBitmap> getResourceClass() { - return PaletteBitmap.class; - } - - @NonNull - @Override - public PaletteBitmap get() { - return paletteBitmap; - } - - @Override - public int getSize() { - return Util.getBitmapByteSize(paletteBitmap.bitmap); - } - - @Override - public void recycle() { - paletteBitmap.bitmap.recycle(); - } -}
\ No newline at end of file diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java deleted file mode 100644 index bcb5f590a..000000000 --- a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example - */ - -package de.danoeh.antennapod.ui.glide; - -import android.graphics.Bitmap; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.palette.graphics.Palette; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.engine.Resource; -import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -public class PaletteBitmapTranscoder implements ResourceTranscoder<Bitmap, PaletteBitmap> { - - @Nullable - @Override - public Resource<PaletteBitmap> transcode(@NonNull Resource<Bitmap> toTranscode, @NonNull Options options) { - Bitmap bitmap = toTranscode.get(); - Palette palette = null; - if (UserPreferences.shouldShowSubscriptionTitle()) { - palette = new Palette.Builder(bitmap).generate(); - } - PaletteBitmap result = new PaletteBitmap(bitmap, palette); - return new PaletteBitmapResource(result); - } -} diff --git a/ui/i18n/build.gradle b/ui/i18n/build.gradle index a1ace417b..4f5370662 100644 --- a/ui/i18n/build.gradle +++ b/ui/i18n/build.gradle @@ -4,7 +4,7 @@ plugins { apply from: "../../common.gradle" android { - lintOptions { + lint { disable "Typos", "ExtraTranslation", "ImpliedQuantity", "PluralsCandidate", "UnusedQuantity", "TypographyEllipsis" } diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index f072ea673..59fc8b4e5 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ <string name="swipe_left">Swipe Left</string> <string name="enable_swipeactions">Enable Swipe Actions for this Screen</string> <string name="change_setting">Change</string> + <string name="individual_subscription">Individual subscription</string> <!-- Statistics fragment --> <string name="statistics_include_marked">Include duration of episodes that are just marked as played</string> @@ -60,6 +61,9 @@ <string name="home_downloads_title">Manage downloads</string> <string name="home_welcome_title">Welcome to AntennaPod!</string> <string name="home_welcome_text">You are not subscribed to any podcasts yet. Open the side menu to add a podcast.</string> + <string name="notification_permission_text">AntennaPod needs your permission to show notifications while downloading episodes.</string> + <string name="notification_permission_denied">You denied the permission.</string> + <string name="open_settings">Open settings</string> <string name="configure_home">Configure Home Screen</string> <!-- Download Statistics fragment --> @@ -103,6 +107,7 @@ <string name="yes">Yes</string> <string name="no">No</string> <string name="reset">Reset</string> + <string name="global_default">Global default</string> <string name="url_label">URL</string> <string name="support_funding_label">Support</string> <string name="support_podcast">Support this Podcast</string> @@ -110,6 +115,7 @@ <string name="error_msg_prefix">An error occurred:</string> <string name="refresh_label">Refresh</string> <string name="chapters_label">Chapters</string> + <string name="no_chapters_label">No chapters</string> <string name="chapter_duration">Duration: %1$s</string> <string name="description_label">Description</string> <string name="shownotes_label">Shownotes</string> @@ -125,9 +131,10 @@ <string name="feed_volume_reduction_light">Light</string> <string name="feed_volume_reduction_heavy">Heavy</string> <string name="parallel_downloads">%1$d parallel downloads</string> - <string name="feed_auto_download_global">Global default</string> <string name="feed_auto_download_always">Always</string> <string name="feed_auto_download_never">Never</string> + <string name="feed_new_episodes_action_add_to_inbox">Add to Inbox</string> + <string name="feed_new_episodes_action_nothing">Nothing</string> <string name="send_label">Send…</string> <string name="episode_cleanup_never">Never</string> <string name="episode_cleanup_except_favorite_removal">When not favorited</string> @@ -175,9 +182,9 @@ <string name="share_label">Share</string> <string name="share_file_label">Share File</string> <string name="share_rss_address_label">RSS address:</string> - <string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes).</string> - <string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts and ALL their episodes (including downloaded episodes).</string> - <string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted.</string> + <string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\", ALL its episodes (including downloaded episodes), and its statistics.</string> + <string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts, ALL their episodes (including downloaded episodes), and its statistics.</string> + <string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\" and its statistics. The files in the local source folder will not be deleted.</string> <string name="feed_remover_msg">Removing podcast</string> <string name="load_complete_feed">Refresh complete podcast</string> <string name="multi_select">Multi select</string> @@ -308,6 +315,7 @@ <string name="confirm_mobile_download_dialog_enable_temporarily">Allow temporarily</string> <!-- Mediaplayer messages --> + <string name="playback_error_generic"><![CDATA[The media file could not be played.\n\n- Try deleting and re-downloading the episode.\n- Check your network connection, and make sure no VPN or login page is blocking access.\n- Try long-pressing and sharing the \"Media address\" to your web browser to see if it can be played there. If not, contact the podcast creators.]]></string> <string name="playback_error_server_died">Server died</string> <string name="playback_error_unsupported">Unsupported media type</string> <string name="playback_error_timeout">Operation timed out</string> @@ -363,9 +371,6 @@ <string name="no_subscriptions_label">To subscribe to a podcast, press the plus icon below.</string> <!-- Preferences --> - <string name="storage_pref">Storage</string> - <string name="storage_sum">Episode auto delete, Import, Export</string> - <string name="statistics_moved">The statistics screen was moved to the subscriptions screen. You can open it from there.</string> <string name="project_pref">Project</string> <string name="synchronization_pref">Synchronization</string> <string name="synchronization_sum">Synchronize with other devices</string> @@ -381,7 +386,6 @@ <string name="preference_search_hint">Search…</string> <string name="preference_search_no_results">No results</string> <string name="preference_search_clear_history">Clear history</string> - <string name="media_player">Media player</string> <string name="pref_episode_cleanup_title">Episode Cleanup</string> <string name="pref_episode_cleanup_summary">Episodes that should be eligible for removal if Auto Download needs space for new episodes</string> <string name="pref_pauseOnDisconnect_sum">Pause playback when headphones or bluetooth are disconnected</string> @@ -406,20 +410,18 @@ <string name="pref_favorite_keeps_episodes_title">Keep Favorite Episodes</string> <string name="playback_pref">Playback</string> <string name="playback_pref_sum">Headphone controls, Skip intervals, Queue</string> - <string name="network_pref">Network</string> - <string name="network_pref_sum">Update interval, Download controls, Mobile data</string> + <string name="downloads_pref">Downloads</string> + <string name="downloads_pref_sum">Update interval, Mobile data, Automatic download, Automatic deletion</string> <string name="feed_refresh_title">Refresh podcasts</string> - <string name="feed_refresh_sum">Specify an interval or a specific time to look for new episodes automatically</string> - <string name="feed_refresh_interval">Interval</string> - <string name="feed_refresh_time">Time</string> + <string name="feed_refresh_sum">Specify an interval at which AntennaPod looks for new episodes automatically</string> <string name="feed_refresh_never">Never</string> - <string name="feed_refresh_interval_at">at %1$s</string> - <plurals name="feed_refresh_every_x_hours"> - <item quantity="one">Every hour</item> - <item quantity="other">Every %d hours</item> - </plurals> - <string name="battery_optimization_pref_title">Battery Optimization</string> - <string name="battery_optimization_pref">For more reliable automatic downloads and automatic refresh, exclude AntennaPod from battery optimization. Tap to add an exception for AntennaPod.</string> + <string name="feed_every_hour">Every hour</string> + <string name="feed_every_2_hours">Every 2 hours</string> + <string name="feed_every_4_hours">Every 4 hours</string> + <string name="feed_every_8_hours">Every 8 hours</string> + <string name="feed_every_12_hours">Every 12 hours</string> + <string name="feed_every_24_hours">Every day</string> + <string name="feed_every_72_hours">Every 3 days</string> <string name="pref_followQueue_title">Continuous Playback</string> <string name="pref_pauseOnHeadsetDisconnect_title">Headphones or Bluetooth disconnect</string> <string name="pref_unpauseOnHeadsetReconnect_title">Headphones Reconnect</string> @@ -435,7 +437,8 @@ <string name="pref_mobileUpdate_streaming">Streaming</string> <string name="user_interface_label">User Interface</string> <string name="user_interface_sum">Appearance, Subscriptions, Lockscreen</string> - <string name="pref_set_theme_title">Select Theme</string> + <string name="pref_black_theme_title">Full Black</string> + <string name="pref_black_theme_message">Use full black for the dark theme</string> <string name="pref_tinted_theme_title">Use dynamic colors</string> <string name="pref_tinted_theme_message">Adapt app colors based on the wallpaper</string> <string name="pref_nav_drawer_items_title">Set Navigation Drawer items</string> @@ -444,7 +447,6 @@ <string name="pref_nav_drawer_feed_order_sum">Change the order of your subscriptions</string> <string name="pref_nav_drawer_feed_counter_title">Set Subscription Counter</string> <string name="pref_nav_drawer_feed_counter_sum">Change the information displayed by the subscription counter. Also affects the sorting of subscriptions if \'Subscription Order\' is set to \'Counter\'.</string> - <string name="pref_set_theme_sum">Change the appearance of AntennaPod.</string> <string name="pref_automatic_download_title">Automatic Download</string> <string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string> <string name="pref_autodl_wifi_filter_title">Enable Wi-Fi filter</string> @@ -458,10 +460,9 @@ <string name="pref_episode_cover_summary">Use the episode specific cover in lists whenever available. If unchecked, the app will always use the podcast cover image.</string> <string name="pref_show_remain_time_title">Show Remaining Time</string> <string name="pref_show_remain_time_summary">Display remaining time of episodes when checked. If unchecked, display total duration of episodes.</string> - <string name="pref_theme_title_use_system">Use system theme</string> + <string name="pref_theme_title_automatic">Automatic</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> - <string name="pref_theme_title_trueblack">Black (AMOLED ready)</string> <string name="pref_episode_cache_unlimited">Unlimited</string> <string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string> <string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string> @@ -485,13 +486,12 @@ <string name="pref_compact_notification_buttons_sum">Change the playback buttons when the notification is collapsed. The play/pause button is always included.</string> <string name="pref_compact_notification_buttons_dialog_title">Select a maximum of %1$d items</string> <string name="pref_compact_notification_buttons_dialog_error">You can only select a maximum of %1$d items.</string> - <string name="pref_lockscreen_background_title">Set Lockscreen Background</string> - <string name="pref_lockscreen_background_sum">Set the lockscreen background to the current episode\'s image. As a side effect, this will also show the image in third party apps.</string> <string name="pref_enqueue_location_title">Enqueue Location</string> <string name="pref_enqueue_location_sum">Add episodes to: %1$s</string> <string name="enqueue_location_back">Back</string> <string name="enqueue_location_front">Front</string> <string name="enqueue_location_after_current">After current episode</string> + <string name="enqueue_location_random">Random</string> <string name="pref_smart_mark_as_played_disabled">Disabled</string> <string name="documentation_support">Documentation & Support</string> <string name="visit_user_forum">User forum</string> @@ -499,19 +499,11 @@ <string name="open_bug_tracker">Open bug tracker</string> <string name="copy_to_clipboard">Copy to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string> - <string name="experimental_pref">Experimental</string> - <string name="pref_media_player_message">Select which media player to use to play files</string> - <string name="pref_current_value">Current value: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Set a network proxy</string> <string name="pref_no_browser_found">No web browser found.</string> <string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string> <string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string> - <string name="media_player_builtin">Built-in Android player (deprecated) </string> - <string name="media_player_sonic">Sonic Media Player (deprecated) </string> - <string name="media_player_exoplayer_recommended">ExoPlayer (recommended)</string> - <string name="media_player_switch_to_exoplayer">Switch to ExoPlayer</string> - <string name="media_player_switched_to_exoplayer">Switched to ExoPlayer.</string> <string name="pref_skip_silence_title">Skip Silence in Audio</string> <string name="behavior">Behavior</string> <string name="pref_default_page">Default Page</string> @@ -535,6 +527,8 @@ <string name="pref_contribute">Contribute</string> <string name="pref_show_subscription_title">Show Subscription Title</string> <string name="pref_show_subscription_title_summary">Display the subscription title below the cover image.</string> + <string name="pref_new_episodes_action_title">New Episodes Action</string> + <string name="pref_new_episodes_action_sum">Action to take for new episodes</string> <!-- About screen --> <string name="about_pref">About</string> @@ -600,6 +594,7 @@ <string name="set_sleeptimer_label">Set sleep timer</string> <string name="disable_sleeptimer_label">Disable sleep timer</string> <string name="extend_sleep_timer_label">+%d min</string> + <string name="sleep_timer_always">Always</string> <string name="sleep_timer_label">Sleep timer</string> <string name="time_dialog_invalid_input">Invalid input, time has to be an integer</string> <string name="shake_to_reset_label">Shake to reset</string> @@ -619,7 +614,9 @@ <item quantity="one">1 hour</item> <item quantity="other">%d hours</item> </plurals> - <string name="auto_enable_label">Auto-enable</string> + <string name="auto_enable_label">Automatically activate the sleep timer when pressing play</string> + <string name="auto_enable_label_with_times">Automatically activate the sleep timer when pressing play between %s and %s</string> + <string name="auto_enable_change_times">Change times</string> <string name="sleep_timer_enabled_label">Sleep timer enabled</string> <!-- Synchronisation --> @@ -696,7 +693,7 @@ <string name="authentication_descr">Change your username and password for this podcast and its episodes.</string> <string name="feed_tags_label">Tags</string> <string name="feed_tags_summary">Change the tags of this podcast to help organize your subscriptions</string> - <string name="feed_folders_include_root">Show in main list</string> + <string name="feed_folders_include_root">Show this podcast in main list</string> <string name="multi_feed_common_tags_info">{fa-info-circle} Only common tags from all selected subscriptions are shown. Other tags stay unaffected.</string> <string name="auto_download_settings_label">Auto Download Settings</string> <string name="episode_filters_label">Episode Filter</string> @@ -723,8 +720,8 @@ <!-- Add podcast fragment --> <string name="search_podcast_hint">Search podcast…</string> - <string name="search_itunes_label">Search iTunes</string> - <string name="search_podcastindex_label">Search Podcastindex.org</string> + <string name="search_itunes_label">Search Apple Podcasts</string> + <string name="search_podcastindex_label">Search Podcast Index</string> <string name="search_fyyd_label">Search fyyd</string> <string name="gpodnet_search_hint">Search gpodder.net</string> <string name="advanced">Advanced</string> @@ -734,7 +731,7 @@ <string name="discover_hide">Hide</string> <string name="discover_is_hidden">You selected to hide suggestions.</string> <string name="discover_more">more »</string> - <string name="discover_powered_by_itunes">Suggestions by iTunes</string> + <string name="discover_powered_by_itunes">Suggestions by Apple Podcasts</string> <string name="discover_confirm">Show suggestions</string> <string name="search_powered_by">Results by %1$s</string> <string name="select_country">Select country</string> @@ -797,9 +794,6 @@ <string name="audio_controls">Audio controls</string> <string name="playback_speed">Playback Speed</string> <string name="audio_effects">Audio Effects</string> - <string name="stereo_to_mono">Downmix: Stereo to mono</string> - <string name="sonic_only">Sonic only</string> - <string name="exoplayer_only">ExoPlayer only</string> <string name="player_switch_to_audio_only">Switch to audio only</string> <!-- proxy settings --> diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml b/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml new file mode 100644 index 000000000..a5480c71f --- /dev/null +++ b/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFFFFFFF" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/> +</vector> diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_playback_speed.xml b/ui/png-icons/src/main/res/drawable/ic_notification_playback_speed.xml new file mode 100644 index 000000000..5aad5031a --- /dev/null +++ b/ui/png-icons/src/main/res/drawable/ic_notification_playback_speed.xml @@ -0,0 +1,8 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="30dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0" + android:width="30dp"> + + <path android:fillColor="#ffffff" android:pathData="M 12 15.98 A 2.98 2.98 0 0 1 9.02 12.99 c 0 -1.11 0.61 -2.09 1.49 -2.6 L 20.17 4.81 L 14.67 14.34 C 14.17 15.31 13.16 15.98 12 15.98 M 12 3.05 c 1.8 0 3.48 0.5 4.94 1.31 l -2.09 1.2 C 13.99 5.22 12.99 5.04 12 5.04 a 7.96 7.96 0 0 0 -7.96 7.96 c 0 2.2 0.89 4.19 2.33 5.62 h 0.01 c 0.39 0.39 0.39 1.01 0 1.4 c -0.39 0.39 -1.02 0.39 -1.41 0.01 v 0 C 3.17 18.22 2.05 15.74 2.05 12.99 A 9.95 9.95 0 0 1 12 3.05 m 9.95 9.95 c 0 2.75 -1.11 5.23 -2.91 7.03 v 0 c -0.39 0.38 -1.01 0.38 -1.4 -0.01 c -0.39 -0.39 -0.39 -1.01 0 -1.4 v 0 c 1.44 -1.44 2.33 -3.42 2.33 -5.62 c 0 -0.99 -0.19 -1.99 -0.54 -2.88 L 20.62 8.02 c 0.83 1.49 1.32 3.16 1.32 4.97 z" /> +</vector> diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java index 8efdcf603..077883321 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java @@ -107,6 +107,10 @@ public class StatisticsFilterDialog { private Pair<String[], Long[]> makeMonthlyList(long oldestDate, boolean inclusive) { Calendar date = Calendar.getInstance(); date.setTimeInMillis(oldestDate); + date.set(Calendar.HOUR_OF_DAY, 0); + date.set(Calendar.MINUTE, 0); + date.set(Calendar.SECOND, 0); + date.set(Calendar.MILLISECOND, 0); date.set(Calendar.DAY_OF_MONTH, 1); ArrayList<String> names = new ArrayList<>(); ArrayList<Long> timestamps = new ArrayList<>(); |