diff options
Diffstat (limited to 'app/src')
147 files changed, 1736 insertions, 5241 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 3c8c5d7f0..21498effd 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -3,8 +3,10 @@ package de.test.antennapod; import android.content.Context; import android.content.Intent; import androidx.annotation.IdRes; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.preference.PreferenceManager; +import androidx.test.espresso.NoMatchingViewException; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.espresso.PerformException; import androidx.test.espresso.UiController; @@ -15,6 +17,9 @@ import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.util.HumanReadables; import androidx.test.espresso.util.TreeIterables; import android.view.View; + +import junit.framework.AssertionFailedError; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -33,6 +38,7 @@ import java.util.concurrent.TimeoutException; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; @@ -57,7 +63,7 @@ public class EspressoTestUtils { @Override public String getDescription() { - return "wait for a specific view for" + millis + " millis."; + return "wait for a specific view for " + millis + " millis."; } @Override @@ -88,6 +94,33 @@ public class EspressoTestUtils { } /** + * Wait until a certain view becomes visible, but at the longest until the timeout. + * Unlike {@link #waitForView(Matcher, long)} it doesn't stick to the initial root view. + * + * @param viewMatcher The view to wait for. + * @param timeoutMillis Maximum waiting period in milliseconds. + * @throws Exception Throws an Exception in case of a timeout. + */ + public static void waitForViewGlobally(@NonNull Matcher<View> viewMatcher, long timeoutMillis) throws Exception { + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeoutMillis; + + do { + try { + onView(viewMatcher).check(matches(isDisplayed())); + // no Exception thrown -> check successful + return; + } catch (NoMatchingViewException | AssertionFailedError ignore) { + // check was not successful "not found" -> continue waiting + } + //noinspection BusyWait + Thread.sleep(50); + } while (System.currentTimeMillis() < endTime); + + throw new Exception("Timeout after " + timeoutMillis + " ms"); + } + + /** * Perform action of waiting for a specific view id. * https://stackoverflow.com/a/30338665/ * @param id The id of the child to click. @@ -113,7 +146,7 @@ public class EspressoTestUtils { } /** - * Clear all app databases + * Clear all app databases. */ public static void clearPreferences() { File root = InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir().getParentFile(); diff --git a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java index 8c628efd5..e31838671 100644 --- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java +++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java @@ -11,15 +11,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.List; - import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -70,7 +66,6 @@ public class ShareDialogTest { onView(withText(R.string.all_episodes_short_label)).perform(click()); Matcher<View> allEpisodesMatcher; - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click())); diff --git a/app/src/androidTest/java/de/test/antennapod/entities/ExternalMediaTest.java b/app/src/androidTest/java/de/test/antennapod/entities/ExternalMediaTest.java deleted file mode 100644 index 83d7a4d22..000000000 --- a/app/src/androidTest/java/de/test/antennapod/entities/ExternalMediaTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.test.antennapod.entities; - -import android.annotation.SuppressLint; -import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.LargeTest; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; -import org.junit.After; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for {@link ExternalMedia} entity. - */ -@SmallTest -public class ExternalMediaTest { - - private static final int NOT_SET = -1; - - @After - public void tearDown() throws Exception { - clearSharedPrefs(); - } - - @SuppressLint("CommitPrefEdits") - private void clearSharedPrefs() { - SharedPreferences prefs = getDefaultSharedPrefs(); - SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); - editor.commit(); - } - - private SharedPreferences getDefaultSharedPrefs() { - return PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().getTargetContext()); - } - - @Test - public void testSaveCurrentPositionUpdatesPreferences() { - final int POSITION = 50; - final int LAST_PLAYED_TIME = 1650; - - assertEquals(NOT_SET, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET)); - assertEquals(NOT_SET, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET)); - - ExternalMedia media = new ExternalMedia("source", MediaType.AUDIO); - media.saveCurrentPosition(getDefaultSharedPrefs(), POSITION, LAST_PLAYED_TIME); - - assertEquals(POSITION, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET)); - assertEquals(LAST_PLAYED_TIME, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET)); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java b/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java deleted file mode 100644 index fc2943205..000000000 --- a/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package de.test.antennapod.feed; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.FeedFilter; -import de.danoeh.antennapod.core.feed.FeedItem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -@SmallTest -public class FeedFilterTest { - - @Test - public void testNullFilter() throws Exception { - FeedFilter filter = new FeedFilter(); - FeedItem item = new FeedItem(); - item.setTitle("Hello world"); - - assertFalse(filter.excludeOnly()); - assertFalse(filter.includeOnly()); - assertEquals("", filter.getExcludeFilter()); - assertEquals("", filter.getIncludeFilter()); - assertTrue(filter.shouldAutoDownload(item)); - } - - @Test - public void testBasicIncludeFilter() throws Exception { - String includeFilter = "Hello"; - FeedFilter filter = new FeedFilter(includeFilter, ""); - FeedItem item = new FeedItem(); - item.setTitle("Hello world"); - - FeedItem item2 = new FeedItem(); - item2.setTitle("Don't include me"); - - assertFalse(filter.excludeOnly()); - assertTrue(filter.includeOnly()); - assertEquals("", filter.getExcludeFilter()); - assertEquals(includeFilter, filter.getIncludeFilter()); - assertTrue(filter.shouldAutoDownload(item)); - assertFalse(filter.shouldAutoDownload(item2)); - } - - @Test - public void testBasicExcludeFilter() throws Exception { - String excludeFilter = "Hello"; - FeedFilter filter = new FeedFilter("", excludeFilter); - FeedItem item = new FeedItem(); - item.setTitle("Hello world"); - - FeedItem item2 = new FeedItem(); - item2.setTitle("Item2"); - - assertTrue(filter.excludeOnly()); - assertFalse(filter.includeOnly()); - assertEquals(excludeFilter, filter.getExcludeFilter()); - assertEquals("", filter.getIncludeFilter()); - assertFalse(filter.shouldAutoDownload(item)); - assertTrue(filter.shouldAutoDownload(item2)); - } - - @Test - public void testComplexIncludeFilter() throws Exception { - String includeFilter = "Hello \n\"Two words\""; - FeedFilter filter = new FeedFilter(includeFilter, ""); - FeedItem item = new FeedItem(); - item.setTitle("hello world"); - - FeedItem item2 = new FeedItem(); - item2.setTitle("Two three words"); - - FeedItem item3 = new FeedItem(); - item3.setTitle("One two words"); - - assertFalse(filter.excludeOnly()); - assertTrue(filter.includeOnly()); - assertEquals("", filter.getExcludeFilter()); - assertEquals(includeFilter, filter.getIncludeFilter()); - assertTrue(filter.shouldAutoDownload(item)); - assertFalse(filter.shouldAutoDownload(item2)); - assertTrue(filter.shouldAutoDownload(item3)); - } - - @Test - public void testComplexExcludeFilter() throws Exception { - String excludeFilter = "Hello \"Two words\""; - FeedFilter filter = new FeedFilter("", excludeFilter); - FeedItem item = new FeedItem(); - item.setTitle("hello world"); - - FeedItem item2 = new FeedItem(); - item2.setTitle("One three words"); - - FeedItem item3 = new FeedItem(); - item3.setTitle("One two words"); - - assertTrue(filter.excludeOnly()); - assertFalse(filter.includeOnly()); - assertEquals(excludeFilter, filter.getExcludeFilter()); - assertEquals("", filter.getIncludeFilter()); - assertFalse(filter.shouldAutoDownload(item)); - assertTrue(filter.shouldAutoDownload(item2)); - assertFalse(filter.shouldAutoDownload(item3)); - } - - @Test - public void testComboFilter() throws Exception { - String includeFilter = "Hello world"; - String excludeFilter = "dislike"; - FeedFilter filter = new FeedFilter(includeFilter, excludeFilter); - - FeedItem download = new FeedItem(); - download.setTitle("Hello everyone!"); - // because, while it has words from the include filter it also has exclude words - FeedItem doNotDownload = new FeedItem(); - doNotDownload.setTitle("I dislike the world"); - // because it has no words from the include filter - FeedItem doNotDownload2 = new FeedItem(); - doNotDownload2.setTitle("no words to include"); - - assertTrue(filter.hasExcludeFilter()); - assertTrue(filter.hasIncludeFilter()); - assertTrue(filter.shouldAutoDownload(download)); - assertFalse(filter.shouldAutoDownload(doNotDownload)); - assertFalse(filter.shouldAutoDownload(doNotDownload2)); - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java b/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java deleted file mode 100644 index 0b9a67d0a..000000000 --- a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.test.antennapod.feed; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.FeedItem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -@SmallTest -public class FeedItemTest { - private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - private static final String TEXT_SHORT = "Lorem ipsum"; - - /** - * If one of `description` or `content:encoded` is null, use the other one. - */ - @Test - public void testShownotesNullValues() throws Exception { - testShownotes(null, TEXT_LONG); - testShownotes(TEXT_LONG, null); - } - - /** - * If `description` is reasonably longer than `content:encoded`, use `description`. - */ - @Test - public void testShownotesLength() throws Exception { - testShownotes(TEXT_SHORT, TEXT_LONG); - testShownotes(TEXT_LONG, TEXT_SHORT); - } - - /** - * Checks if the shownotes equal TEXT_LONG, using the given `description` and `content:encoded` - * @param description Description of the feed item - * @param contentEncoded `content:encoded` of the feed item - */ - private void testShownotes(String description, String contentEncoded) throws Exception { - FeedItem item = new FeedItem(); - item.setDescription(description); - item.setContentEncoded(contentEncoded); - assertEquals(TEXT_LONG, item.loadShownotes().call()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java deleted file mode 100644 index de9f53ae2..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for Atom feeds in FeedHandler. - */ -@SmallTest -public class AtomParserTest extends FeedParserTestBase { - @Test - public void testAtomBasic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0); - feedValid(f1, f2, Feed.TYPE_ATOM1); - } - - @Test - public void testLogoWithWhitespace() throws Exception { - String logo = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new AtomGenerator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "logo"); - xml.text(" " + logo + "\n"); - xml.endTag(null, "logo"); - } - }, "UTF-8", 0); - assertEquals(logo, f2.getImageUrl()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java deleted file mode 100644 index 83f334633..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java +++ /dev/null @@ -1,154 +0,0 @@ -package de.test.antennapod.handler; - -import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; -import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; -import org.junit.After; -import org.junit.Before; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for FeedHandler. - */ -public abstract class FeedParserTestBase { - private static final String FEEDS_DIR = "testfeeds"; - - private File file = null; - private OutputStream outputStream = null; - - @Before - public void setUp() throws Exception { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - File destDir = context.getExternalFilesDir(FEEDS_DIR); - assertNotNull(destDir); - - file = new File(destDir, "feed.xml"); - file.delete(); - - assertNotNull(file); - assertFalse(file.exists()); - - outputStream = new FileOutputStream(file); - } - - - @After - public void tearDown() throws Exception { - file.delete(); - file = null; - - outputStream.close(); - outputStream = null; - } - - protected Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags) - throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { - g.writeFeed(feed, outputStream, encoding, flags); - FeedHandler handler = new FeedHandler(); - Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate()); - parsedFeed.setFile_url(file.getAbsolutePath()); - parsedFeed.setDownloaded(true); - handler.parseFeed(parsedFeed); - return parsedFeed; - } - - protected void feedValid(Feed feed, Feed parsedFeed, String feedType) { - assertEquals(feed.getTitle(), parsedFeed.getTitle()); - if (feedType.equals(Feed.TYPE_ATOM1)) { - assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier()); - } else { - assertEquals(feed.getLanguage(), parsedFeed.getLanguage()); - } - - assertEquals(feed.getLink(), parsedFeed.getLink()); - assertEquals(feed.getDescription(), parsedFeed.getDescription()); - assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink()); - assertEquals(feed.getImageUrl(), parsedFeed.getImageUrl()); - - if (feed.getItems() != null) { - assertNotNull(parsedFeed.getItems()); - assertEquals(feed.getItems().size(), parsedFeed.getItems().size()); - - for (int i = 0; i < feed.getItems().size(); i++) { - FeedItem item = feed.getItems().get(i); - FeedItem parsedItem = parsedFeed.getItems().get(i); - - if (item.getItemIdentifier() != null) { - assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier()); - } - assertEquals(item.getTitle(), parsedItem.getTitle()); - assertEquals(item.getDescription(), parsedItem.getDescription()); - assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded()); - assertEquals(item.getLink(), parsedItem.getLink()); - assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime()); - assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink()); - - if (item.hasMedia()) { - assertTrue(parsedItem.hasMedia()); - FeedMedia media = item.getMedia(); - FeedMedia parsedMedia = parsedItem.getMedia(); - - assertEquals(media.getDownload_url(), parsedMedia.getDownload_url()); - assertEquals(media.getSize(), parsedMedia.getSize()); - assertEquals(media.getMime_type(), parsedMedia.getMime_type()); - } - - assertEquals(feed.getImageUrl(), item.getImageLocation()); - - if (item.getChapters() != null) { - assertNotNull(parsedItem.getChapters()); - assertEquals(item.getChapters().size(), parsedItem.getChapters().size()); - List<Chapter> chapters = item.getChapters(); - List<Chapter> parsedChapters = parsedItem.getChapters(); - for (int j = 0; j < chapters.size(); j++) { - Chapter chapter = chapters.get(j); - Chapter parsedChapter = parsedChapters.get(j); - - assertEquals(chapter.getTitle(), parsedChapter.getTitle()); - assertEquals(chapter.getLink(), parsedChapter.getLink()); - } - } - } - } - } - - protected Feed createTestFeed(int numItems, boolean withFeedMedia) { - Feed feed = new Feed(0, null, "title", "http://example.com", "This is the description", - "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", - "http://example.com/picture", file.getAbsolutePath(), "http://example.com/feed", true); - feed.setItems(new ArrayList<>()); - - for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i, - "http://example.com/items/" + i, new Date(i * 60000), FeedItem.UNPLAYED, feed); - feed.getItems().add(item); - if (withFeedMedia) { - item.setMedia(new FeedMedia(0, item, 4711, 0, 1024 * 1024, "audio/mp3", null, - "http://example.com/media-" + i, false, null, 0, 0)); - } - } - - return feed; - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java deleted file mode 100644 index c2e319233..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.syndication.namespace.NSMedia; -import de.test.antennapod.util.syndication.feedgenerator.Rss2Generator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for RSS feeds in FeedHandler. - */ -@SmallTest -public class RssParserTest extends FeedParserTestBase { - @Test - public void testRss2Basic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new Rss2Generator(), "UTF-8", Rss2Generator.FEATURE_WRITE_GUID); - feedValid(f1, f2, Feed.TYPE_RSS2); - } - - @Test - public void testImageWithWhitespace() throws Exception { - String image = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "image"); - xml.startTag(null, "url"); - xml.text(" " + image + "\n"); - xml.endTag(null, "url"); - xml.endTag(null, "image"); - } - }, "UTF-8", 0); - assertEquals(image, f2.getImageUrl()); - } - - @Test - public void testMediaContentMime() throws Exception { - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.setPrefix(NSMedia.NSTAG, NSMedia.NSURI); - xml.startTag(null, "item"); - xml.startTag(NSMedia.NSURI, "content"); - xml.attribute(null, "url", "https://www.example.com/file.mp4"); - xml.attribute(null, "medium", "video"); - xml.endTag(NSMedia.NSURI, "content"); - xml.endTag(null, "item"); - } - }, "UTF-8", 0); - assertEquals(MediaType.VIDEO, f2.getItems().get(0).getMedia().getMediaType()); - } -} 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 419cf2096..f3bd61839 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -10,6 +10,7 @@ import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; @@ -252,7 +253,7 @@ public class PlaybackTest { onView(isRoot()).perform(waitForView(withText(R.string.all_episodes_short_label), 1000)); onView(withText(R.string.all_episodes_short_label)).perform(click()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); Matcher<View> allEpisodesMatcher = allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton))); @@ -287,7 +288,7 @@ public class PlaybackTest { uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); activityTestRule.launchActivity(new Intent()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); startLocalPlayback(); FeedMedia media = episodes.get(0).getMedia(); diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java index fd395f7c1..70cf4166b 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java @@ -17,10 +17,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.feed.Feed; diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java index f039c8bdf..ddd4fe899 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -6,6 +6,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.filters.LargeTest; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.core.widget.WidgetUpdater; import org.awaitility.Awaitility; import org.greenrobot.eventbus.EventBus; import org.junit.After; @@ -187,8 +188,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -248,8 +249,9 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { + public WidgetUpdater.WidgetState requestWidgetState() { countDownLatch.countDown(); + return null; } @Override @@ -348,8 +350,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -391,8 +393,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -449,8 +451,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override diff --git a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java index 5396b218d..e74cf49b7 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java @@ -3,15 +3,13 @@ package de.test.antennapod.storage; import android.content.Context; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.DBTasksCallbacks; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -31,8 +29,7 @@ public class AutoDownloadTest { private Context context; private UITestUtils stubFeedsServer; - - private DBTasksCallbacks dbTasksCallbacksOrig; + private StubDownloadAlgorithm stubDownloadAlgorithm; @Before public void setUp() throws Exception { @@ -41,16 +38,19 @@ public class AutoDownloadTest { stubFeedsServer = new UITestUtils(context); stubFeedsServer.setup(); - dbTasksCallbacksOrig = ClientConfig.dbTasksCallbacks; - EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); UserPreferences.setAllowMobileStreaming(true); + + // Setup: enable automatic download + // it is not needed, as the actual automatic download is stubbed. + stubDownloadAlgorithm = new StubDownloadAlgorithm(); + DBTasks.setDownloadAlgorithm(stubDownloadAlgorithm); } @After public void tearDown() throws Exception { - ClientConfig.dbTasksCallbacks = dbTasksCallbacksOrig; + DBTasks.setDownloadAlgorithm(new AutomaticDownloadAlgorithm()); EspressoTestUtils.tryKillPlaybackService(); stubFeedsServer.tearDown(); } @@ -76,11 +76,6 @@ public class AutoDownloadTest { FeedItem item0 = queue.get(0); FeedItem item1 = queue.get(1); - // Setup: enable automatic download - // it is not needed, as the actual automatic download is stubbed. - StubDownloadAlgorithm stubDownloadAlgorithm = new StubDownloadAlgorithm(); - useDownloadAlgorithm(stubDownloadAlgorithm); - // Actual test // Play the first one in the queue playEpisode(item0); @@ -94,11 +89,10 @@ public class AutoDownloadTest { } catch (ConditionTimeoutException cte) { long actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload(); fail("when auto download is triggered, the next episode should be playing: (" - + item1.getId() + ", " + item1.getTitle() + ") . " + + item1.getId() + ", " + item1.getTitle() + ") . " + "Actual playing: (" + actual + ")" ); } - } private void playEpisode(@NonNull FeedItem item) { @@ -113,21 +107,7 @@ public class AutoDownloadTest { .until(() -> item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); } - private void useDownloadAlgorithm(final AutomaticDownloadAlgorithm downloadAlgorithm) { - ClientConfig.dbTasksCallbacks = new DBTasksCallbacks() { - @Override - public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm() { - return downloadAlgorithm; - } - - @Override - public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm() { - return dbTasksCallbacksOrig.getEpisodeCacheCleanupAlgorithm(); - } - }; - } - - private static class StubDownloadAlgorithm implements AutomaticDownloadAlgorithm { + private static class StubDownloadAlgorithm extends AutomaticDownloadAlgorithm { private long currentlyPlaying = -1; @Override diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBCleanupTests.java b/app/src/androidTest/java/de/test/antennapod/storage/DBCleanupTests.java deleted file mode 100644 index 6c36da13e..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBCleanupTests.java +++ /dev/null @@ -1,217 +0,0 @@ -package de.test.antennapod.storage; - -import android.content.Context; -import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.LargeTest; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for DBTasks - */ -@SmallTest -public class DBCleanupTests { - static final int EPISODE_CACHE_SIZE = 5; - private int cleanupAlgorithm; - - Context context; - - private File destFolder; - - public DBCleanupTests() { - setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_DEFAULT); - } - - protected void setCleanupAlgorithm(int cleanupAlgorithm) { - this.cleanupAlgorithm = cleanupAlgorithm; - } - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - - cleanupDestFolder(destFolder); - assertTrue(destFolder.delete()); - } - - private void cleanupDestFolder(File destFolder) { - for (File f : destFolder.listFiles()) { - assertTrue(f.delete()); - } - } - - @Before - public void setUp() throws Exception { - context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - destFolder = new File(context.getCacheDir(), "DDCleanupTests"); - destFolder.mkdir(); - cleanupDestFolder(destFolder); - assertNotNull(destFolder); - assertTrue(destFolder.exists()); - assertTrue(destFolder.canWrite()); - - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); - prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE)); - prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, Integer.toString(cleanupAlgorithm)); - prefEdit.putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true); - prefEdit.commit(); - - UserPreferences.init(context); - } - - @Test - public void testPerformAutoCleanupShouldDelete() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, false, false); - - DBTasks.performAutoCleanup(context); - for (int i = 0; i < files.size(); i++) { - if (i < EPISODE_CACHE_SIZE) { - assertTrue(files.get(i).exists()); - } else { - assertFalse(files.get(i).exists()); - } - } - } - - void populateItems(final int numItems, Feed feed, List<FeedItem> items, - List<File> files, int itemState, boolean addToQueue, - boolean addToFavorites) throws IOException { - for (int i = 0; i < numItems; i++) { - Date itemDate = new Date(numItems - i); - Date playbackCompletionDate = null; - if (itemState == FeedItem.PLAYED) { - playbackCompletionDate = itemDate; - } - FeedItem item = new FeedItem(0, "title", "id", "link", itemDate, itemState, feed); - - File f = new File(destFolder, "file " + i); - assertTrue(f.createNewFile()); - files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, playbackCompletionDate, 0, 0)); - items.add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - if (addToQueue) { - adapter.setQueue(items); - } - if (addToFavorites) { - adapter.setFavorites(items); - } - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : items) { - assertTrue(item.getId() != 0); - assertTrue(item.getMedia().getId() != 0); - } - } - - @Test - public void testPerformAutoCleanupHandleUnplayed() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.UNPLAYED, false, false); - - DBTasks.performAutoCleanup(context); - for (File file : files) { - assertTrue(file.exists()); - } - } - - @Test - public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, true, false); - - DBTasks.performAutoCleanup(context); - for (File file : files) { - assertTrue(file.exists()); - } - } - - /** - * Reproduces a bug where DBTasks.performAutoCleanup(android.content.Context) would use the ID of the FeedItem in the - * call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia. This would cause the wrong item to be deleted. - * @throws IOException - */ - @Test - public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException { - // add feed with no enclosures so that item ID != media ID - saveFeedlist(1, 10, false); - - // add candidate for performAutoCleanup - List<Feed> feeds = saveFeedlist(1, 1, true); - FeedMedia m = feeds.get(0).getItems().get(0).getMedia(); - m.setDownloaded(true); - m.setFile_url("file"); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setMedia(m); - adapter.close(); - - testPerformAutoCleanupShouldNotDeleteBecauseInQueue(); - } - - @Test - public void testPerformAutoCleanupShouldNotDeleteBecauseFavorite() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, false, true); - - DBTasks.performAutoCleanup(context); - for (File file : files) { - assertTrue(file.exists()); - } - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBNullCleanupAlgorithmTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBNullCleanupAlgorithmTest.java deleted file mode 100644 index d7ebf2351..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBNullCleanupAlgorithmTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package de.test.antennapod.storage; - -import android.content.Context; -import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.LargeTest; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests that the APNullCleanupAlgorithm is working correctly. - */ -@SmallTest -public class DBNullCleanupAlgorithmTest { - - private static final String TAG = "DBNullCleanupAlgorithmTest"; - private static final int EPISODE_CACHE_SIZE = 5; - - private Context context; - - private File destFolder; - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - - cleanupDestFolder(destFolder); - assertTrue(destFolder.delete()); - } - - private void cleanupDestFolder(File destFolder) { - for (File f : destFolder.listFiles()) { - assertTrue(f.delete()); - } - } - - @Before - public void setUp() throws Exception { - context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - destFolder = context.getExternalCacheDir(); - cleanupDestFolder(destFolder); - assertNotNull(destFolder); - assertTrue(destFolder.exists()); - assertTrue(destFolder.canWrite()); - - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); - prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE)); - prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, Integer.toString(UserPreferences.EPISODE_CLEANUP_NULL)); - prefEdit.commit(); - - UserPreferences.init(context); - } - - /** - * A test with no items in the queue, but multiple items downloaded. - * The null algorithm should never delete any items, even if they're played and not in the queue. - * @throws IOException - */ - @Test - public void testPerformAutoCleanupShouldNotDelete() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); - - File f = new File(destFolder, "file " + i); - assertTrue(f.createNewFile()); - files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, - new Date(NUM_ITEMS - i), 0, 0)); - items.add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : items) { - assertTrue(item.getId() != 0); - assertTrue(item.getMedia().getId() != 0); - } - DBTasks.performAutoCleanup(context); - for (int i = 0; i < files.size(); i++) { - assertTrue(files.get(i).exists()); - } - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBQueueCleanupAlgorithmTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBQueueCleanupAlgorithmTest.java deleted file mode 100644 index de810c701..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBQueueCleanupAlgorithmTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.test.antennapod.storage; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * Tests that the APQueueCleanupAlgorithm is working correctly. - */ -@SmallTest -public class DBQueueCleanupAlgorithmTest extends DBCleanupTests { - - private static final String TAG = "DBQueueCleanupAlgorithmTest"; - - public DBQueueCleanupAlgorithmTest() { - setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_QUEUE); - } - - /** - * For APQueueCleanupAlgorithm we expect even unplayed episodes to be deleted if needed - * if they aren't in the queue - */ - @Test - public void testPerformAutoCleanupHandleUnplayed() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.UNPLAYED, false, false); - - DBTasks.performAutoCleanup(context); - for (int i = 0; i < files.size(); i++) { - if (i < EPISODE_CACHE_SIZE) { - assertTrue(files.get(i).exists()); - } else { - assertFalse(files.get(i).exists()); - } - } - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java deleted file mode 100644 index 8811d9b31..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java +++ /dev/null @@ -1,411 +0,0 @@ -package de.test.antennapod.storage; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Random; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.LongList; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for DBReader - */ -@SmallTest -public class DBReaderTest { - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - } - - @Before - public void setUp() throws Exception { - // create new database - PodDBAdapter.init(InstrumentationRegistry.getInstrumentation().getTargetContext()); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - } - - @Test - public void testGetFeedList() { - List<Feed> feeds = saveFeedlist(10, 0, false); - List<Feed> savedFeeds = DBReader.getFeedList(); - assertNotNull(savedFeeds); - assertEquals(feeds.size(), savedFeeds.size()); - for (int i = 0; i < feeds.size(); i++) { - assertEquals(feeds.get(i).getId(), savedFeeds.get(i).getId()); - } - } - - @Test - public void testGetFeedListSortOrder() { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - - Feed feed1 = new Feed(0, null, "A", "link", "d", null, null, null, "rss", "A", null, "", "", true); - Feed feed2 = new Feed(0, null, "b", "link", "d", null, null, null, "rss", "b", null, "", "", true); - Feed feed3 = new Feed(0, null, "C", "link", "d", null, null, null, "rss", "C", null, "", "", true); - Feed feed4 = new Feed(0, null, "d", "link", "d", null, null, null, "rss", "d", null, "", "", true); - adapter.setCompleteFeed(feed1); - adapter.setCompleteFeed(feed2); - adapter.setCompleteFeed(feed3); - adapter.setCompleteFeed(feed4); - assertTrue(feed1.getId() != 0); - assertTrue(feed2.getId() != 0); - assertTrue(feed3.getId() != 0); - assertTrue(feed4.getId() != 0); - - adapter.close(); - - List<Feed> saved = DBReader.getFeedList(); - assertNotNull(saved); - assertEquals("Wrong size: ", 4, saved.size()); - - assertEquals("Wrong id of feed 1: ", feed1.getId(), saved.get(0).getId()); - assertEquals("Wrong id of feed 2: ", feed2.getId(), saved.get(1).getId()); - assertEquals("Wrong id of feed 3: ", feed3.getId(), saved.get(2).getId()); - assertEquals("Wrong id of feed 4: ", feed4.getId(), saved.get(3).getId()); - } - - @Test - public void testFeedListDownloadUrls() { - List<Feed> feeds = saveFeedlist(10, 0, false); - List<String> urls = DBReader.getFeedListDownloadUrls(); - assertNotNull(urls); - assertEquals(feeds.size(), urls.size()); - for (int i = 0; i < urls.size(); i++) { - assertEquals(urls.get(i), feeds.get(i).getDownload_url()); - } - } - - @Test - public void testLoadFeedDataOfFeedItemlist() { - final int numFeeds = 10; - final int numItems = 1; - List<Feed> feeds = saveFeedlist(numFeeds, numItems, false); - List<FeedItem> items = new ArrayList<>(); - for (Feed f : feeds) { - for (FeedItem item : f.getItems()) { - item.setFeed(null); - item.setFeedId(f.getId()); - items.add(item); - } - } - DBReader.loadAdditionalFeedItemListData(items); - for (int i = 0; i < numFeeds; i++) { - for (int j = 0; j < numItems; j++) { - FeedItem item = feeds.get(i).getItems().get(j); - assertNotNull(item.getFeed()); - assertEquals(feeds.get(i).getId(), item.getFeed().getId()); - assertEquals(item.getFeed().getId(), item.getFeedId()); - } - } - } - - @Test - public void testGetFeedItemList() { - final int numFeeds = 1; - final int numItems = 10; - Feed feed = saveFeedlist(numFeeds, numItems, false).get(0); - List<FeedItem> items = feed.getItems(); - feed.setItems(null); - List<FeedItem> savedItems = DBReader.getFeedItemList(feed); - assertNotNull(savedItems); - assertEquals(items.size(), savedItems.size()); - for (int i = 0; i < savedItems.size(); i++) { - assertEquals(savedItems.get(i).getId(), items.get(i).getId()); - } - } - - private List<FeedItem> saveQueue(int numItems) { - if (numItems <= 0) { - throw new IllegalArgumentException("numItems<=0"); - } - List<Feed> feeds = saveFeedlist(numItems, numItems, false); - List<FeedItem> allItems = new ArrayList<>(); - for (Feed f : feeds) { - allItems.addAll(f.getItems()); - } - // take random items from every feed - Random random = new Random(); - List<FeedItem> queue = new ArrayList<>(); - while (queue.size() < numItems) { - int index = random.nextInt(numItems); - if (!queue.contains(allItems.get(index))) { - queue.add(allItems.get(index)); - } - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - return queue; - } - - @Test - public void testGetQueueIDList() { - final int numItems = 10; - List<FeedItem> queue = saveQueue(numItems); - LongList ids = DBReader.getQueueIDList(); - assertNotNull(ids); - assertEquals(ids.size(), queue.size()); - for (int i = 0; i < queue.size(); i++) { - assertTrue(ids.get(i) != 0); - assertEquals(ids.get(i), queue.get(i).getId()); - } - } - - @Test - public void testGetQueue() { - final int numItems = 10; - List<FeedItem> queue = saveQueue(numItems); - List<FeedItem> savedQueue = DBReader.getQueue(); - assertNotNull(savedQueue); - assertEquals(savedQueue.size(), queue.size()); - for (int i = 0; i < queue.size(); i++) { - assertTrue(savedQueue.get(i).getId() != 0); - assertEquals(savedQueue.get(i).getId(), queue.get(i).getId()); - } - } - - private List<FeedItem> saveDownloadedItems(int numItems) { - if (numItems <= 0) { - throw new IllegalArgumentException("numItems<=0"); - } - List<Feed> feeds = saveFeedlist(numItems, numItems, true); - List<FeedItem> items = new ArrayList<>(); - for (Feed f : feeds) { - items.addAll(f.getItems()); - } - List<FeedItem> downloaded = new ArrayList<>(); - Random random = new Random(); - - while (downloaded.size() < numItems) { - int i = random.nextInt(numItems); - if (!downloaded.contains(items.get(i))) { - FeedItem item = items.get(i); - item.getMedia().setDownloaded(true); - item.getMedia().setFile_url("file" + i); - downloaded.add(item); - } - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemlist(downloaded); - adapter.close(); - return downloaded; - } - - @Test - public void testGetDownloadedItems() { - final int numItems = 10; - List<FeedItem> downloaded = saveDownloadedItems(numItems); - List<FeedItem> downloaded_saved = DBReader.getDownloadedItems(); - assertNotNull(downloaded_saved); - assertEquals(downloaded.size(), downloaded_saved.size()); - for (FeedItem item : downloaded_saved) { - assertNotNull(item.getMedia()); - assertTrue(item.getMedia().isDownloaded()); - assertNotNull(item.getMedia().getDownload_url()); - } - } - - private List<FeedItem> saveNewItems(int numItems) { - List<Feed> feeds = saveFeedlist(numItems, numItems, true); - List<FeedItem> items = new ArrayList<>(); - for (Feed f : feeds) { - items.addAll(f.getItems()); - } - List<FeedItem> newItems = new ArrayList<>(); - Random random = new Random(); - - while (newItems.size() < numItems) { - int i = random.nextInt(numItems); - if (!newItems.contains(items.get(i))) { - FeedItem item = items.get(i); - item.setNew(); - newItems.add(item); - } - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemlist(newItems); - adapter.close(); - return newItems; - } - - @Test - public void testGetNewItemIds() { - final int numItems = 10; - - List<FeedItem> newItems = saveNewItems(numItems); - long[] unreadIds = new long[newItems.size()]; - for (int i = 0; i < newItems.size(); i++) { - unreadIds[i] = newItems.get(i).getId(); - } - List<FeedItem> newItemsSaved = DBReader.getNewItemsList(0, Integer.MAX_VALUE); - assertNotNull(newItemsSaved); - assertEquals(newItemsSaved.size(), newItems.size()); - for (FeedItem feedItem : newItemsSaved) { - long savedId = feedItem.getId(); - boolean found = false; - for (long id : unreadIds) { - if (id == savedId) { - found = true; - break; - } - } - assertTrue(found); - } - } - - @Test - public void testGetPlaybackHistory() { - final int numItems = (DBReader.PLAYBACK_HISTORY_SIZE + 1) * 2; - final int playedItems = DBReader.PLAYBACK_HISTORY_SIZE + 1; - final int numReturnedItems = Math.min(playedItems, DBReader.PLAYBACK_HISTORY_SIZE); - final int numFeeds = 1; - - Feed feed = DBTestUtils.saveFeedlist(numFeeds, numItems, true).get(0); - long[] ids = new long[playedItems]; - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - for (int i = 0; i < playedItems; i++) { - FeedMedia m = feed.getItems().get(i).getMedia(); - m.setPlaybackCompletionDate(new Date(i + 1)); - adapter.setFeedMediaPlaybackCompletionDate(m); - ids[ids.length - 1 - i] = m.getItem().getId(); - } - adapter.close(); - - List<FeedItem> saved = DBReader.getPlaybackHistory(); - assertNotNull(saved); - assertEquals("Wrong size: ", numReturnedItems, saved.size()); - for (int i = 0; i < numReturnedItems; i++) { - FeedItem item = saved.get(i); - assertNotNull(item.getMedia().getPlaybackCompletionDate()); - assertEquals("Wrong sort order: ", item.getId(), ids[i]); - } - } - - @Test - public void testGetNavDrawerDataQueueEmptyNoUnreadItems() { - final int NUM_FEEDS = 10; - final int NUM_ITEMS = 10; - DBTestUtils.saveFeedlist(NUM_FEEDS, NUM_ITEMS, true); - DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(); - assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); - assertEquals(0, navDrawerData.numNewItems); - assertEquals(0, navDrawerData.queueSize); - } - - @Test - public void testGetNavDrawerDataQueueNotEmptyWithUnreadItems() { - final int NUM_FEEDS = 10; - final int NUM_ITEMS = 10; - final int NUM_QUEUE = 1; - final int NUM_NEW = 2; - List<Feed> feeds = DBTestUtils.saveFeedlist(NUM_FEEDS, NUM_ITEMS, true); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - for (int i = 0; i < NUM_NEW; i++) { - FeedItem item = feeds.get(0).getItems().get(i); - item.setNew(); - adapter.setSingleFeedItem(item); - } - List<FeedItem> queue = new ArrayList<>(); - for (int i = 0; i < NUM_QUEUE; i++) { - FeedItem item = feeds.get(1).getItems().get(i); - queue.add(item); - } - adapter.setQueue(queue); - - adapter.close(); - - DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(); - assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); - assertEquals(NUM_NEW, navDrawerData.numNewItems); - assertEquals(NUM_QUEUE, navDrawerData.queueSize); - } - - @Test - public void testGetFeedItemlistCheckChaptersFalse() throws Exception { - List<Feed> feeds = DBTestUtils.saveFeedlist(10, 10, false, false, 0); - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - assertFalse(item.hasChapters()); - } - } - } - - @Test - public void testGetFeedItemlistCheckChaptersTrue() throws Exception { - List<Feed> feeds = saveFeedlist(10, 10, false, true, 10); - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - assertTrue(item.hasChapters()); - } - } - } - - @Test - public void testLoadChaptersOfFeedItemNoChapters() throws Exception { - List<Feed> feeds = saveFeedlist(1, 3, false, false, 0); - saveFeedlist(1, 3, false, true, 3); - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - assertFalse(item.hasChapters()); - item.setChapters(DBReader.loadChaptersOfFeedItem(item)); - assertFalse(item.hasChapters()); - assertNull(item.getChapters()); - } - } - } - - @Test - public void testLoadChaptersOfFeedItemWithChapters() throws Exception { - final int NUM_CHAPTERS = 3; - DBTestUtils.saveFeedlist(1, 3, false, false, 0); - List<Feed> feeds = saveFeedlist(1, 3, false, true, NUM_CHAPTERS); - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - assertTrue(item.hasChapters()); - item.setChapters(DBReader.loadChaptersOfFeedItem(item)); - assertTrue(item.hasChapters()); - assertNotNull(item.getChapters()); - assertEquals(NUM_CHAPTERS, item.getChapters().size()); - } - } - } - - @Test - public void testGetItemWithChapters() throws Exception { - final int NUM_CHAPTERS = 3; - List<Feed> feeds = saveFeedlist(1, 1, false, true, NUM_CHAPTERS); - FeedItem item1 = feeds.get(0).getItems().get(0); - FeedItem item2 = DBReader.getFeedItem(item1.getId()); - item2.setChapters(DBReader.loadChaptersOfFeedItem(item2)); - assertTrue(item2.hasChapters()); - assertEquals(item1.getChapters(), item2.getChapters()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java deleted file mode 100644 index c28ce5003..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ /dev/null @@ -1,289 +0,0 @@ -package de.test.antennapod.storage; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -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.PodDBAdapter; - -import static de.danoeh.antennapod.core.util.FeedItemUtil.getIdList; -import static java.util.Collections.singletonList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -/** - * Test class for DBTasks - */ -@SmallTest -public class DBTasksTest { - private Context context; - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - } - - @Before - public void setUp() throws Exception { - context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - UserPreferences.init(context); - } - - @Test - public void testUpdateFeedNewFeed() { - final int NUM_ITEMS = 10; - - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed)); - } - Feed newFeed = DBTasks.updateFeed(context, feed, false); - - assertEquals(feed.getId(), newFeed.getId()); - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertFalse(item.isPlayed()); - assertTrue(item.getId() != 0); - } - } - - /** Two feeds with the same title, but different download URLs should be treated as different feeds. */ - @Test - public void testUpdateFeedSameTitle() { - - Feed feed1 = new Feed("url1", null, "title"); - Feed feed2 = new Feed("url2", null, "title"); - - feed1.setItems(new ArrayList<>()); - feed2.setItems(new ArrayList<>()); - - Feed savedFeed1 = DBTasks.updateFeed(context, feed1, false); - Feed savedFeed2 = DBTasks.updateFeed(context, feed2, false); - - assertTrue(savedFeed1.getId() != savedFeed2.getId()); - } - - @Test - public void testUpdateFeedUpdatedFeed() { - final int NUM_ITEMS_OLD = 10; - final int NUM_ITEMS_NEW = 10; - - final Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS_OLD; i++) { - feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed)); - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - // ensure that objects have been saved in db, then reset - assertTrue(feed.getId() != 0); - final long feedID = feed.getId(); - feed.setId(0); - List<Long> itemIDs = new ArrayList<>(); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - itemIDs.add(item.getId()); - item.setId(0); - } - - for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) { - feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.UNPLAYED, feed)); - } - - final Feed newFeed = DBTasks.updateFeed(context, feed, false); - assertNotSame(newFeed, feed); - - updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); - - final Feed feedFromDB = DBReader.getFeed(newFeed.getId()); - assertNotNull(feedFromDB); - assertEquals(newFeed.getId(), feedFromDB.getId()); - updatedFeedTest(feedFromDB, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); - } - - @Test - public void testUpdateFeedMediaUrlResetState() { - final Feed feed = new Feed("url", null, "title"); - FeedItem item = new FeedItem(0, "item", "id", "link", new Date(), FeedItem.PLAYED, feed); - feed.setItems(singletonList(item)); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - // ensure that objects have been saved in db, then reset - assertTrue(feed.getId() != 0); - assertTrue(item.getId() != 0); - - FeedMedia media = new FeedMedia(item, "url", 1024, "mime/type"); - item.setMedia(media); - List<FeedItem> list = new ArrayList<>(); - list.add(item); - feed.setItems(list); - - final Feed newFeed = DBTasks.updateFeed(context, feed, false); - assertNotSame(newFeed, feed); - - final Feed feedFromDB = DBReader.getFeed(newFeed.getId()); - final FeedItem feedItemFromDB = feedFromDB.getItems().get(0); - assertTrue("state: " + feedItemFromDB.getState(), feedItemFromDB.isNew()); - } - - @Test - public void testUpdateFeedRemoveUnlistedItems() { - final Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < 10; i++) { - feed.getItems().add( - new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed)); - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - // delete some items - feed.getItems().subList(0, 2).clear(); - Feed newFeed = DBTasks.updateFeed(context, feed, true); - assertEquals(8, newFeed.getItems().size()); // 10 - 2 = 8 items - - Feed feedFromDB = DBReader.getFeed(newFeed.getId()); - assertEquals(8, feedFromDB.getItems().size()); // 10 - 2 = 8 items - } - - private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) { - assertEquals(feedID, newFeed.getId()); - assertEquals(NUM_ITEMS_NEW + NUM_ITEMS_OLD, newFeed.getItems().size()); - Collections.reverse(newFeed.getItems()); - Date lastDate = new Date(0); - for (int i = 0; i < NUM_ITEMS_OLD; i++) { - FeedItem item = newFeed.getItems().get(i); - assertSame(newFeed, item.getFeed()); - assertEquals((long) itemIDs.get(i), item.getId()); - assertTrue(item.isPlayed()); - assertTrue(item.getPubDate().getTime() >= lastDate.getTime()); - lastDate = item.getPubDate(); - } - for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) { - FeedItem item = newFeed.getItems().get(i); - assertSame(newFeed, item.getFeed()); - assertTrue(item.getId() != 0); - assertFalse(item.isPlayed()); - assertTrue(item.getPubDate().getTime() >= lastDate.getTime()); - lastDate = item.getPubDate(); - } - } - - @Test - public void testAddQueueItemsInDownload_EnqueueEnabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(true); - UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - // the first item fis1.get(0) is already in the queue - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Arrays.asList(fis1.get(1), fis2.get(2), fis2.get(1)); - List<FeedItem> expectedQueue = new ArrayList<>(); - expectedQueue.addAll(DBReader.getQueue()); - expectedQueue.addAll(expectedEnqueued); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("Only items not in the queue are enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue()); - } - - @Test - public void testAddQueueItemsInDownload_EnqueueDisabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(false); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Collections.emptyList(); - List<FeedItem> expectedQueue = DBReader.getQueue(); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue()); - } - - private void assertEqualsByIds(String msg, List<? extends FeedItem> expected, List<? extends FeedItem> actual) { - // assert only the IDs, so that any differences are easily to spot. - List<Long> expectedIds = getIdList(expected); - List<Long> actualIds = getIdList(actual); - assertEquals(msg, expectedIds, actualIds); - } - - private Feed createSavedFeed(String title, int numFeedItems) { - final Feed feed = new Feed("url", null, title); - - if (numFeedItems > 0) { - List<FeedItem> items = new ArrayList<>(numFeedItems); - for (int i = 1; i <= numFeedItems; i++) { - FeedItem item = new FeedItem(0, "item " + i + " of " + title, "id", "link", - new Date(), FeedItem.UNPLAYED, feed); - items.add(item); - } - feed.setItems(items); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - return feed; - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java deleted file mode 100644 index 840a7d01f..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java +++ /dev/null @@ -1,79 +0,0 @@ -package de.test.antennapod.storage; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.SimpleChapter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; - -import static org.junit.Assert.assertTrue; - -/** - * Utility methods for DB* tests. - */ -class DBTestUtils { - - private DBTestUtils(){} - /** - * Use this method when tests don't involve chapters. - */ - public static List<Feed> saveFeedlist(int numFeeds, int numItems, boolean withMedia) { - return saveFeedlist(numFeeds, numItems, withMedia, false, 0); - } - - /** - * Use this method when tests involve chapters. - */ - public static List<Feed> saveFeedlist(int numFeeds, int numItems, boolean withMedia, - boolean withChapters, int numChapters) { - if (numFeeds <= 0) { - throw new IllegalArgumentException("numFeeds<=0"); - } - if (numItems < 0) { - throw new IllegalArgumentException("numItems<0"); - } - - List<Feed> feeds = new ArrayList<>(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - for (int i = 0; i < numFeeds; i++) { - Feed f = new Feed(0, null, "feed " + i, "link" + i, "descr", null, null, - null, null, "id" + i, null, null, "url" + i, false); - f.setItems(new ArrayList<>()); - for (int j = 0; j < numItems; j++) { - FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), - FeedItem.PLAYED, f, withChapters); - if (withMedia) { - FeedMedia media = new FeedMedia(item, "url" + j, 1, "audio/mp3"); - item.setMedia(media); - } - if (withChapters) { - List<Chapter> chapters = new ArrayList<>(); - item.setChapters(chapters); - for (int k = 0; k < numChapters; k++) { - chapters.add(new SimpleChapter(k, "item " + j + " chapter " + k, - "http://example.com", "http://example.com/image.png")); - } - } - f.getItems().add(item); - } - Collections.sort(f.getItems(), new FeedItemPubdateComparator()); - adapter.setCompleteFeed(f); - assertTrue(f.getId() != 0); - for (FeedItem item : f.getItems()) { - assertTrue(item.getId() != 0); - } - feeds.add(f); - } - adapter.close(); - - return feeds; - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java deleted file mode 100644 index 652389d00..000000000 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java +++ /dev/null @@ -1,857 +0,0 @@ -package de.test.antennapod.storage; - -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import androidx.preference.PreferenceManager; -import android.util.Log; - -import androidx.core.util.Consumer; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.MediumTest; - -import org.awaitility.Awaitility; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.FeedItemUtil; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for DBWriter - */ -@MediumTest -public class DBWriterTest { - - private static final String TAG = "DBWriterTest"; - private static final String TEST_FOLDER = "testDBWriter"; - private static final long TIMEOUT = 5L; - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - File testDir = context.getExternalFilesDir(TEST_FOLDER); - assertNotNull(testDir); - for (File f : testDir.listFiles()) { - f.delete(); - } - } - - @Before - public void setUp() throws Exception { - // create new database - PodDBAdapter.init(InstrumentationRegistry.getInstrumentation().getTargetContext()); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); - prefEdit.putBoolean(UserPreferences.PREF_DELETE_REMOVES_FROM_QUEUE, true).commit(); - - UserPreferences.init(context); - } - - @Test - public void testSetFeedMediaPlaybackInformation() - throws IOException, ExecutionException, InterruptedException, TimeoutException { - final int POSITION = 50; - final long LAST_PLAYED_TIME = 1000; - final int PLAYED_DURATION = 60; - final int DURATION = 100; - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed); - items.add(item); - FeedMedia media = new FeedMedia(0, item, DURATION, 1, 1, "mime_type", "dummy path", "download_url", true, null, 0, 0); - item.setMedia(media); - - DBWriter.setFeedItem(item).get(TIMEOUT, TimeUnit.SECONDS); - - media.setPosition(POSITION); - media.setLastPlayedTime(LAST_PLAYED_TIME); - media.setPlayedDuration(PLAYED_DURATION); - - DBWriter.setFeedMediaPlaybackInformation(item.getMedia()).get(TIMEOUT, TimeUnit.SECONDS); - - FeedItem itemFromDb = DBReader.getFeedItem(item.getId()); - FeedMedia mediaFromDb = itemFromDb.getMedia(); - - assertEquals(POSITION, mediaFromDb.getPosition()); - assertEquals(LAST_PLAYED_TIME, mediaFromDb.getLastPlayedTime()); - assertEquals(PLAYED_DURATION, mediaFromDb.getPlayedDuration()); - assertEquals(DURATION, mediaFromDb.getDuration()); - } - - @Test - public void testDeleteFeedMediaOfItemFileExists() - throws IOException, ExecutionException, InterruptedException, TimeoutException { - File dest = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); - - assertTrue(dest.createNewFile()); - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed); - - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0, 0); - item.setMedia(media); - - items.add(item); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - assertTrue(media.getId() != 0); - assertTrue(item.getId() != 0); - - DBWriter.deleteFeedMediaOfItem(InstrumentationRegistry.getInstrumentation().getTargetContext(), media.getId()) - .get(TIMEOUT, TimeUnit.SECONDS); - media = DBReader.getFeedMedia(media.getId()); - assertNotNull(media); - assertFalse(dest.exists()); - assertFalse(media.isDownloaded()); - assertNull(media.getFile_url()); - } - - @Test - public void testDeleteFeedMediaOfItemRemoveFromQueue() - throws IOException, ExecutionException, InterruptedException, TimeoutException { - assertTrue(UserPreferences.shouldDeleteRemoveFromQueue()); - - File dest = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); - - assertTrue(dest.createNewFile()); - - Feed feed = new Feed("url", null, "title"); - List<FeedItem> items = new ArrayList<>(); - List<FeedItem> queue = new ArrayList<>(); - feed.setItems(items); - FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.UNPLAYED, feed); - - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0, 0); - item.setMedia(media); - - items.add(item); - queue.add(item); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.setQueue(queue); - adapter.close(); - assertTrue(media.getId() != 0); - assertTrue(item.getId() != 0); - queue = DBReader.getQueue(); - assertTrue(queue.size() != 0); - - DBWriter.deleteFeedMediaOfItem(InstrumentationRegistry.getInstrumentation().getTargetContext(), media.getId()); - Awaitility.await().until(() -> !dest.exists()); - media = DBReader.getFeedMedia(media.getId()); - assertNotNull(media); - assertFalse(dest.exists()); - assertFalse(media.isDownloaded()); - assertNull(media.getFile_url()); - queue = DBReader.getQueue(); - assertEquals(0, queue.size()); - } - - @Test - public void testDeleteFeed() throws ExecutionException, InterruptedException, IOException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); - assertNotNull(destFolder); - - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - - List<File> itemFiles = new ArrayList<>(); - // create items with downloaded media files - for (int i = 0; i < 10; i++) { - FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - - File enc = new File(destFolder, "file " + i); - assertTrue(enc.createNewFile()); - - itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0, 0); - item.setMedia(media); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - assertTrue(item.getMedia().getId() != 0); - } - - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - - // check if files still exist - for (File f : itemFiles) { - assertFalse(f.exists()); - } - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor c = adapter.getFeedCursor(feed.getId()); - assertEquals(0, c.getCount()); - c.close(); - for (FeedItem item : feed.getItems()) { - c = adapter.getFeedItemCursor(String.valueOf(item.getId())); - assertEquals(0, c.getCount()); - c.close(); - c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); - assertEquals(0, c.getCount()); - c.close(); - } - adapter.close(); - } - - @Test - public void testDeleteFeedNoItems() throws IOException, ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); - assertNotNull(destFolder); - - Feed feed = new Feed("url", null, "title"); - feed.setItems(null); - feed.setImageUrl("url"); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor c = adapter.getFeedCursor(feed.getId()); - assertEquals(0, c.getCount()); - c.close(); - adapter.close(); - } - - @Test - public void testDeleteFeedNoFeedMedia() throws IOException, ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); - assertNotNull(destFolder); - - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - - feed.setImageUrl("url"); - - // create items - for (int i = 0; i < 10; i++) { - FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor c = adapter.getFeedCursor(feed.getId()); - assertEquals(0, c.getCount()); - c.close(); - for (FeedItem item : feed.getItems()) { - c = adapter.getFeedItemCursor(String.valueOf(item.getId())); - assertEquals(0, c.getCount()); - c.close(); - } - adapter.close(); - } - - @Test - public void testDeleteFeedWithQueueItems() throws ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); - assertNotNull(destFolder); - - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - - feed.setImageUrl("url"); - - // create items with downloaded media files - for (int i = 0; i < 10; i++) { - FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - File enc = new File(destFolder, "file " + i); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0, 0); - item.setMedia(media); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - assertTrue(item.getMedia().getId() != 0); - } - - - List<FeedItem> queue = new ArrayList<>(feed.getItems()); - adapter.open(); - adapter.setQueue(queue); - - Cursor queueCursor = adapter.getQueueIDCursor(); - assertEquals(queue.size(), queueCursor.getCount()); - queueCursor.close(); - - adapter.close(); - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - adapter.open(); - - Cursor c = adapter.getFeedCursor(feed.getId()); - assertEquals(0, c.getCount()); - c.close(); - for (FeedItem item : feed.getItems()) { - c = adapter.getFeedItemCursor(String.valueOf(item.getId())); - assertEquals(0, c.getCount()); - c.close(); - c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); - assertEquals(0, c.getCount()); - c.close(); - } - c = adapter.getQueueCursor(); - assertEquals(0, c.getCount()); - c.close(); - adapter.close(); - } - - @Test - public void testDeleteFeedNoDownloadedFiles() throws ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); - assertNotNull(destFolder); - - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - - feed.setImageUrl("url"); - - // create items with downloaded media files - for (int i = 0; i < 10; i++) { - FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - File enc = new File(destFolder, "file " + i); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0, 0); - item.setMedia(media); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - assertTrue(item.getMedia().getId() != 0); - } - - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor c = adapter.getFeedCursor(feed.getId()); - assertEquals(0, c.getCount()); - c.close(); - for (FeedItem item : feed.getItems()) { - c = adapter.getFeedItemCursor(String.valueOf(item.getId())); - assertEquals(0, c.getCount()); - c.close(); - c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); - assertEquals(0, c.getCount()); - c.close(); - } - adapter.close(); - } - - @Test - public void testDeleteFeedItems() throws Exception { - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - feed.setImageUrl("url"); - - // create items - for (int i = 0; i < 10; i++) { - FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - List<FeedItem> itemsToDelete = feed.getItems().subList(0, 2); - DBWriter.deleteFeedItems(InstrumentationRegistry.getInstrumentation() - .getTargetContext(), itemsToDelete).get(TIMEOUT, TimeUnit.SECONDS); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - for (int i = 0; i < feed.getItems().size(); i++) { - FeedItem feedItem = feed.getItems().get(i); - Cursor c = adapter.getFeedItemCursor(String.valueOf(feedItem.getId())); - if (i < 2) { - assertEquals(0, c.getCount()); - } else { - assertEquals(1, c.getCount()); - } - c.close(); - } - adapter.close(); - } - - private FeedMedia playbackHistorySetup(Date playbackCompletionDate) { - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); - FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate, 0, 0); - feed.getItems().add(item); - item.setMedia(media); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - assertTrue(media.getId() != 0); - return media; - } - - @Test - public void testAddItemToPlaybackHistoryNotPlayedYet() - throws ExecutionException, InterruptedException, TimeoutException { - FeedMedia media = playbackHistorySetup(null); - DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - media = DBReader.getFeedMedia(media.getId()); - adapter.close(); - - assertNotNull(media); - assertNotNull(media.getPlaybackCompletionDate()); - } - - @Test - public void testAddItemToPlaybackHistoryAlreadyPlayed() - throws ExecutionException, InterruptedException, TimeoutException { - final long OLD_DATE = 0; - - FeedMedia media = playbackHistorySetup(new Date(OLD_DATE)); - DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - media = DBReader.getFeedMedia(media.getId()); - adapter.close(); - - assertNotNull(media); - assertNotNull(media.getPlaybackCompletionDate()); - assertNotEquals(media.getPlaybackCompletionDate().getTime(), OLD_DATE); - } - - private Feed queueTestSetupMultipleItems(final int numItems) throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK); - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - List<Future<?>> futures = new ArrayList<>(); - for (FeedItem item : feed.getItems()) { - futures.add(DBWriter.addQueueItem(context, item)); - } - for (Future<?> f : futures) { - f.get(TIMEOUT, TimeUnit.SECONDS); - } - return feed; - } - - @Test - public void testAddQueueItemSingleItem() throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(item.getId() != 0); - DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor cursor = adapter.getQueueIDCursor(); - assertTrue(cursor.moveToFirst()); - assertEquals(item.getId(), cursor.getLong(0)); - cursor.close(); - adapter.close(); - } - - @Test - public void testAddQueueItemSingleItemAlreadyInQueue() throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(item.getId() != 0); - DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor cursor = adapter.getQueueIDCursor(); - assertTrue(cursor.moveToFirst()); - assertEquals(item.getId(), cursor.getLong(0)); - cursor.close(); - adapter.close(); - - DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS); - adapter = PodDBAdapter.getInstance(); - adapter.open(); - cursor = adapter.getQueueIDCursor(); - assertTrue(cursor.moveToFirst()); - assertEquals(item.getId(), cursor.getLong(0)); - assertEquals(1, cursor.getCount()); - cursor.close(); - adapter.close(); - } - - @Test - public void testAddQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - - Feed feed = queueTestSetupMultipleItems(NUM_ITEMS); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor cursor = adapter.getQueueIDCursor(); - assertTrue(cursor.moveToFirst()); - assertEquals(NUM_ITEMS, cursor.getCount()); - List<Long> expectedIds = FeedItemUtil.getIdList(feed.getItems()); - List<Long> actualIds = new ArrayList<>(); - for (int i = 0; i < NUM_ITEMS; i++) { - assertTrue(cursor.moveToPosition(i)); - actualIds.add(cursor.getLong(0)); - } - cursor.close(); - adapter.close(); - assertEquals("Bulk add to queue: result order should be the same as the order given", - expectedIds, actualIds); - } - - @Test - public void testClearQueue() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - - queueTestSetupMultipleItems(NUM_ITEMS); - DBWriter.clearQueue().get(TIMEOUT, TimeUnit.SECONDS); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor cursor = adapter.getQueueIDCursor(); - assertFalse(cursor.moveToFirst()); - cursor.close(); - adapter.close(); - } - - @Test - public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = createTestFeed(NUM_ITEMS); - - for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) { - final FeedItem item = feed.getItems().get(removeIndex); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setQueue(feed.getItems()); - adapter.close(); - - DBWriter.removeQueueItem(context, false, item).get(TIMEOUT, TimeUnit.SECONDS); - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor queue = adapter.getQueueIDCursor(); - assertEquals(NUM_ITEMS - 1, queue.getCount()); - for (int i = 0; i < queue.getCount(); i++) { - assertTrue(queue.moveToPosition(i)); - final long queueID = queue.getLong(0); - assertTrue(queueID != item.getId()); // removed item is no longer in queue - boolean idFound = false; - for (FeedItem other : feed.getItems()) { // items that were not removed are still in the queue - idFound = idFound | (other.getId() == queueID); - } - assertTrue(idFound); - } - queue.close(); - adapter.close(); - } - } - - @Test - public void testRemoveQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { - // Setup test data - // - final int NUM_ITEMS = 5; - final int NUM_IN_QUEUE = NUM_ITEMS - 1; // the last one not in queue for boundary condition - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = createTestFeed(NUM_ITEMS); - - List<FeedItem> itemsToAdd = feed.getItems().subList(0, NUM_IN_QUEUE); - withPodDB(adapter -> adapter.setQueue(itemsToAdd) ); - - // Actual tests - // - - // Use array rather than List to make codes more succinct - Long[] itemIds = toItemIds(feed.getItems()).toArray(new Long[0]); - - DBWriter.removeQueueItem(context, false, - itemIds[1], itemIds[3]).get(TIMEOUT, TimeUnit.SECONDS); - assertQueueByItemIds("Average case - 2 items removed successfully", - itemIds[0], itemIds[2]); - - DBWriter.removeQueueItem(context, false).get(TIMEOUT, TimeUnit.SECONDS); - assertQueueByItemIds("Boundary case - no items supplied. queue should see no change", - itemIds[0], itemIds[2]); - - DBWriter.removeQueueItem(context, false, - itemIds[0], itemIds[4], -1L).get(TIMEOUT, TimeUnit.SECONDS); - assertQueueByItemIds("Boundary case - items not in queue ignored", - itemIds[2]); - - DBWriter.removeQueueItem(context, false, - itemIds[2], -1L).get(TIMEOUT, TimeUnit.SECONDS); - assertQueueByItemIds("Boundary case - invalid itemIds ignored"); // the queue is empty - - } - - @Test - public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - for (int from = 0; from < NUM_ITEMS; from++) { - for (int to = 0; to < NUM_ITEMS; to++) { - if (from == to) { - continue; - } - Log.d(TAG, String.format(Locale.US, "testMoveQueueItem: From=%d, To=%d", from, to)); - final long fromID = feed.getItems().get(from).getId(); - - adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setQueue(feed.getItems()); - adapter.close(); - - DBWriter.moveQueueItem(from, to, false).get(TIMEOUT, TimeUnit.SECONDS); - adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor queue = adapter.getQueueIDCursor(); - assertEquals(NUM_ITEMS, queue.getCount()); - assertTrue(queue.moveToPosition(from)); - assertNotEquals(fromID, queue.getLong(0)); - assertTrue(queue.moveToPosition(to)); - assertEquals(fromID, queue.getLong(0)); - - queue.close(); - adapter.close(); - } - } - } - - @Test - public void testMarkFeedRead() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - - DBWriter.markFeedRead(feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - List<FeedItem> loadedItems = DBReader.getFeedItemList(feed); - for (FeedItem item : loadedItems) { - assertTrue(item.isPlayed()); - } - } - - @Test - public void testMarkAllItemsReadSameFeed() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); - - assertTrue(feed.getId() != 0); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - - DBWriter.markAllItemsRead().get(TIMEOUT, TimeUnit.SECONDS); - List<FeedItem> loadedItems = DBReader.getFeedItemList(feed); - for (FeedItem item : loadedItems) { - assertTrue(item.isPlayed()); - } - } - - private static Feed createTestFeed(int numItems) { - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - } - - withPodDB(adapter -> adapter.setCompleteFeed(feed)); - - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } - return feed; - } - - private static void withPodDB(Consumer<PodDBAdapter> action) { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - try { - adapter.open(); - action.accept(adapter); - } finally { - adapter.close(); - } - } - - private static void assertQueueByItemIds( - String message, - long... itemIdsExpected - ) { - List<FeedItem> queue = DBReader.getQueue(); - List<Long> itemIdsActualList = toItemIds(queue); - List<Long> itemIdsExpectedList = new ArrayList<>(itemIdsExpected.length); - for (long id : itemIdsExpected) { - itemIdsExpectedList.add(id); - } - - assertEquals(message, itemIdsExpectedList, itemIdsActualList); - } - - private static List<Long> toItemIds(List<FeedItem> items) { - List<Long> itemIds = new ArrayList<>(items.size()); - for(FeedItem item : items) { - itemIds.add(item.getId()); - } - return itemIds; - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index be1ed6cc2..417a78f02 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -2,16 +2,14 @@ package de.test.antennapod.ui; import android.app.Activity; import android.content.Intent; -import androidx.test.platform.app.InstrumentationRegistry; + import androidx.test.espresso.Espresso; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + import com.robotium.solo.Solo; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.test.antennapod.EspressoTestUtils; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -20,6 +18,12 @@ import org.junit.runner.RunWith; import java.io.IOException; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.test.antennapod.EspressoTestUtils; + import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -28,18 +32,17 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.contrib.ActivityResultMatchers.hasResultCode; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.openNavDrawer; -import static de.test.antennapod.EspressoTestUtils.waitForView; -import static junit.framework.TestCase.assertTrue; +import static de.test.antennapod.EspressoTestUtils.waitForViewGlobally; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; /** - * User interface tests for MainActivity + * User interface tests for MainActivity. */ @RunWith(AndroidJUnit4.class) public class MainActivityTest { @@ -48,19 +51,19 @@ public class MainActivityTest { private UITestUtils uiTestUtils; @Rule - public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); + public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(MainActivity.class, false, false); @Before public void setUp() throws IOException { EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); - mActivityRule.launchActivity(new Intent()); + activityRule.launchActivity(new Intent()); uiTestUtils = new UITestUtils(InstrumentationRegistry.getInstrumentation().getTargetContext()); uiTestUtils.setup(); - solo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivityRule.getActivity()); + solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityRule.getActivity()); } @After @@ -71,16 +74,22 @@ public class MainActivityTest { @Test public void testAddFeed() throws Exception { + // connect to podcast feed uiTestUtils.addHostedFeedData(); final Feed feed = uiTestUtils.hostedFeeds.get(0); openNavDrawer(); onView(withText(R.string.add_feed_label)).perform(click()); - onView(withId(R.id.btn_add_via_url)).perform(scrollTo(), click()); - onView(withId(R.id.text)).perform(replaceText(feed.getDownload_url())); + onView(withId(R.id.addViaUrlButton)).perform(scrollTo(), click()); + onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownload_url())); onView(withText(R.string.confirm_label)).perform(scrollTo(), click()); + + // subscribe podcast Espresso.closeSoftKeyboard(); + waitForViewGlobally(withText(R.string.subscribe_label), 15000); onView(withText(R.string.subscribe_label)).perform(click()); - onView(isRoot()).perform(waitForView(withId(R.id.butShowSettings), 5000)); + + // wait for podcast feed item list + waitForViewGlobally(withId(R.id.butShowSettings), 15000); } @Test @@ -100,7 +109,7 @@ public class MainActivityTest { onView(allOf(withId(R.id.toolbar), isDisplayed())).check( matches(hasDescendant(withText(R.string.subscriptions_label)))); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -113,7 +122,7 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertTrue(((MainActivity)solo.getCurrentActivity()).isDrawerOpen()); + assertTrue(((MainActivity) solo.getCurrentActivity()).isDrawerOpen()); } @Test @@ -127,7 +136,7 @@ public class MainActivityTest { solo.goBack(); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -142,7 +151,7 @@ public class MainActivityTest { solo.goBack(); onView(withText(R.string.yes)).perform(click()); Thread.sleep(100); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -155,6 +164,6 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java index ade5ea298..53396372a 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java @@ -42,9 +42,9 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.onDrawerItem; import static de.test.antennapod.EspressoTestUtils.waitForView; import static de.test.antennapod.NthMatcher.first; -import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * User interface tests for MainActivity drawer. 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 9bf89980c..bba546a88 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -15,6 +15,7 @@ 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.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; @@ -43,9 +44,9 @@ 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 junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertTrue; @LargeTest public class PreferencesTest { @@ -372,6 +373,17 @@ public class PreferencesTest { } @Test + public void testEpisodeCleanupFavoriteOnly() { + clickPreference(R.string.network_pref); + onView(withText(R.string.pref_automatic_download_title)).perform(click()); + onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); + onView(isRoot()).perform(waitForView(withText(R.string.episode_cleanup_except_favorite_removal), 1000)); + onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click()); + Awaitility.await().atMost(1000, MILLISECONDS) + .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm); + } + + @Test public void testEpisodeCleanupQueueOnly() { clickPreference(R.string.network_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java index 634904f71..5b291752d 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java @@ -1,15 +1,12 @@ package de.test.antennapod.ui; import android.content.Intent; -import android.view.View; -import androidx.test.espresso.Espresso; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.fragment.QueueFragment; import de.test.antennapod.EspressoTestUtils; -import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java index 5f79e935c..739efcfd2 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java @@ -14,8 +14,8 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.IgnoreOnCi; import org.awaitility.Awaitility; @@ -37,8 +37,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static de.test.antennapod.EspressoTestUtils.waitForView; -import static de.test.antennapod.NthMatcher.first; -import static org.hamcrest.Matchers.allOf; /** * User interface tests for changing the playback speed. @@ -74,7 +72,7 @@ public class SpeedChangeTest { UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f)); EspressoTestUtils.tryKillPlaybackService(); - activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); + activityRule.launchActivity(new Intent().putExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, true)); controller = new PlaybackController(activityRule.getActivity()); controller.init(); controller.getMedia(); // To load media diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java index 60516454f..54592df0b 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java @@ -6,7 +6,6 @@ import java.net.URL; import java.util.List; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.LargeTest; import androidx.test.filters.MediumTest; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; diff --git a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java deleted file mode 100644 index 93e5bcb74..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package de.test.antennapod.util; - -import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import android.text.TextUtils; - -import java.io.File; -import java.io.IOException; - -import de.danoeh.antennapod.core.util.FileNameGenerator; -import org.apache.commons.lang3.StringUtils; -import org.junit.After; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -@SmallTest -public class FilenameGeneratorTest { - - public FilenameGeneratorTest() { - super(); - } - - @Test - public void testGenerateFileName() throws IOException { - String result = FileNameGenerator.generateFileName("abc abc"); - assertEquals(result, "abc abc"); - createFiles(result); - } - - @Test - public void testGenerateFileName1() throws IOException { - String result = FileNameGenerator.generateFileName("ab/c: <abc"); - assertEquals(result, "abc abc"); - createFiles(result); - } - - @Test - public void testGenerateFileName2() throws IOException { - String result = FileNameGenerator.generateFileName("abc abc "); - assertEquals(result, "abc abc"); - createFiles(result); - } - - @Test - public void testFeedTitleContainsApostrophe() { - String result = FileNameGenerator.generateFileName("Feed's Title ..."); - assertEquals("Feeds Title", result); - } - - @Test - public void testFeedTitleContainsDash() { - String result = FileNameGenerator.generateFileName("Left - Right"); - assertEquals("Left - Right", result); - } - - @Test - public void testFeedTitleContainsAccents() { - String result = FileNameGenerator.generateFileName("Äà áâãå"); - assertEquals("Aaaaaa", result); - } - - @Test - public void testInvalidInput() { - String result = FileNameGenerator.generateFileName("???"); - assertFalse(TextUtils.isEmpty(result)); - } - - @Test - public void testLongFilename() throws IOException { - String longName = StringUtils.repeat("x", 20 + FileNameGenerator.MAX_FILENAME_LENGTH); - String result = FileNameGenerator.generateFileName(longName); - assertTrue(result.length() <= FileNameGenerator.MAX_FILENAME_LENGTH); - createFiles(result); - } - - @Test - public void testLongFilenameNotEquals() { - // Verify that the name is not just trimmed and different suffixes end up with the same name - String longName = StringUtils.repeat("x", 20 + FileNameGenerator.MAX_FILENAME_LENGTH); - String result1 = FileNameGenerator.generateFileName(longName + "a"); - String result2 = FileNameGenerator.generateFileName(longName + "b"); - assertNotEquals(result1, result2); - } - - /** - * Tests if files can be created. - * - * @throws IOException - */ - private void createFiles(String name) throws IOException { - File cache = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalCacheDir(); - File testFile = new File(cache, name); - testFile.mkdir(); - assertTrue(testFile.exists()); - testFile.delete(); - assertTrue(testFile.createNewFile()); - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java deleted file mode 100644 index 7f26ff612..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package de.test.antennapod.util; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.util.URLChecker; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * Test class for URLChecker - */ -@SmallTest -public class URLCheckerTest { - - @Test - public void testCorrectURLHttp() { - final String in = "http://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals(in, out); - } - - @Test - public void testCorrectURLHttps() { - final String in = "https://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals(in, out); - } - - @Test - public void testMissingProtocol() { - final String in = "example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testFeedProtocol() { - final String in = "feed://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testPcastProtocolNoScheme() { - final String in = "pcast://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testItpcProtocol() { - final String in = "itpc://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testItpcProtocolWithScheme() { - final String in = "itpc://https://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("https://example.com", out); - } - - @Test - public void testWhiteSpaceUrlShouldNotAppend() { - final String in = "\n http://example.com \t"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testWhiteSpaceShouldAppend() { - final String in = "\n example.com \t"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testAntennaPodSubscribeProtocolNoScheme() throws Exception { - final String in = "antennapod-subscribe://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("http://example.com", out); - } - - @Test - public void testPcastProtocolWithScheme() { - final String in = "pcast://https://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("https://example.com", out); - } - - @Test - public void testAntennaPodSubscribeProtocolWithScheme() throws Exception { - final String in = "antennapod-subscribe://https://example.com"; - final String out = URLChecker.prepareURL(in); - assertEquals("https://example.com", out); - } - - @Test - public void testProtocolRelativeUrlIsAbsolute() throws Exception { - final String in = "https://example.com"; - final String inBase = "http://examplebase.com"; - final String out = URLChecker.prepareURL(in, inBase); - assertEquals(in, out); - } - - @Test - public void testProtocolRelativeUrlIsRelativeHttps() throws Exception { - final String in = "//example.com"; - final String inBase = "https://examplebase.com"; - final String out = URLChecker.prepareURL(in, inBase); - assertEquals("https://example.com", out); - } - - @Test - public void testProtocolRelativeUrlIsHttpsWithAPSubscribeProtocol() throws Exception { - final String in = "//example.com"; - final String inBase = "antennapod-subscribe://https://examplebase.com"; - final String out = URLChecker.prepareURL(in, inBase); - assertEquals("https://example.com", out); - } - - @Test - public void testProtocolRelativeUrlBaseUrlNull() throws Exception { - final String in = "example.com"; - final String out = URLChecker.prepareURL(in, null); - assertEquals("http://example.com", out); - } - - @Test - public void testUrlEqualsSame() { - assertTrue(URLChecker.urlEquals("https://www.example.com/test", "https://www.example.com/test")); - assertTrue(URLChecker.urlEquals("https://www.example.com/test", "https://www.example.com/test/")); - assertTrue(URLChecker.urlEquals("https://www.example.com/test", "https://www.example.com//test")); - assertTrue(URLChecker.urlEquals("https://www.example.com", "https://www.example.com/")); - assertTrue(URLChecker.urlEquals("https://www.example.com", "http://www.example.com")); - assertTrue(URLChecker.urlEquals("http://www.example.com/", "https://www.example.com/")); - assertTrue(URLChecker.urlEquals("https://www.example.com/?id=42", "https://www.example.com/?id=42")); - assertTrue(URLChecker.urlEquals("https://example.com/podcast%20test", "https://example.com/podcast test")); - assertTrue(URLChecker.urlEquals("https://example.com/?a=podcast%20test", "https://example.com/?a=podcast test")); - assertTrue(URLChecker.urlEquals("https://example.com/?", "https://example.com/")); - assertTrue(URLChecker.urlEquals("https://example.com/?", "https://example.com")); - assertTrue(URLChecker.urlEquals("https://Example.com", "https://example.com")); - assertTrue(URLChecker.urlEquals("https://example.com/test", "https://example.com/Test")); - } - - @Test - public void testUrlEqualsDifferent() { - assertFalse(URLChecker.urlEquals("https://www.example.com/test", "https://www.example2.com/test")); - assertFalse(URLChecker.urlEquals("https://www.example.com/test", "https://www.example.de/test")); - assertFalse(URLChecker.urlEquals("https://example.com/", "https://otherpodcast.example.com/")); - assertFalse(URLChecker.urlEquals("https://www.example.com/?id=42&a=b", "https://www.example.com/?id=43&a=b")); - assertFalse(URLChecker.urlEquals("https://example.com/podcast%25test", "https://example.com/podcast test")); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java deleted file mode 100644 index ed37b7daa..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package de.test.antennapod.util.playback; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.util.Date; -import java.util.List; - -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.playback.Playable; -import de.danoeh.antennapod.core.util.playback.Timeline; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for timeline. - */ -@SmallTest -public class TimelineTest { - - private Context context; - - @Before - public void setUp() { - context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - } - - private Playable newTestPlayable(List<Chapter> chapters, String shownotes, int duration) { - FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), FeedItem.PLAYED, null); - item.setChapters(chapters); - item.setContentEncoded(shownotes); - FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3"); - media.setDuration(duration); - item.setMedia(media); - return media; - } - - @Test - public void testProcessShownotesAddTimecodeHHMMSSNoChapters() { - final String timeStr = "10:11:12"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11 + 12 * 1000; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHHMMSSMoreThen24HoursNoChapters() { - final String timeStr = "25:00:00"; - final long time = 25 * 60 * 60 * 1000; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHHMMNoChapters() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeMMSSNoChapters() { - final String timeStr = "10:11"; - final long time = 10 * 60 * 1000 + 11 * 1000; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", 11 * 60 * 1000); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHMMSSNoChapters() { - final String timeStr = "2:11:12"; - final long time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeMSSNoChapters() { - final String timeStr = "1:12"; - final long time = 60 * 1000 + 12 * 1000; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", 2 * 60 * 1000); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddNoTimecodeDuration() { - final String timeStr = "2:11:12"; - final int time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - String originalText = "<p> Some test text with a timecode " + timeStr + " here.</p>"; - Playable p = newTestPlayable(null, originalText, time); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - Document d = Jsoup.parse(res); - assertEquals("Should not parse time codes that equal duration", 0, d.body().getElementsByTag("a").size()); - } - - @Test - public void testProcessShownotesAddTimecodeMultipleFormatsNoChapters() { - final String[] timeStrings = new String[]{ "10:12", "1:10:12" }; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 2 * 60 * 60 * 1000); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, - 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000 }, timeStrings); - } - - @Test - public void testProcessShownotesAddTimecodeMultipleShortFormatNoChapters() { - - // One of these timecodes fits as HH:MM and one does not so both should be parsed as MM:SS. - final String[] timeStrings = new String[]{ "10:12", "2:12" }; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 3 * 60 * 60 * 1000); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000 }, timeStrings); - } - - @Test - public void testProcessShownotesAddTimecodeParentheses() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode (" - + timeStr + ") here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeBrackets() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode [" - + timeStr + "] here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeAngleBrackets() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - Playable p = newTestPlayable(null, "<p> Some test text with a timecode <" - + timeStr + "> here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAndInvalidTimecode() { - final String[] timeStrs = new String[] {"2:1", "0:0", "000", "00", "00:000"}; - - StringBuilder shownotes = new StringBuilder("<p> Some test text with timecodes "); - for (String timeStr : timeStrs) { - shownotes.append(timeStr).append(" "); - } - shownotes.append("here.</p>"); - - Playable p = newTestPlayable(null, shownotes.toString(), Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[0], new String[0]); - } - - private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) { - assertNotNull(res); - Document d = Jsoup.parse(res); - Elements links = d.body().getElementsByTag("a"); - int countedLinks = 0; - for (Element link : links) { - String href = link.attributes().get("href"); - String text = link.text(); - if (href.startsWith("antennapod://")) { - assertTrue(href.endsWith(String.valueOf(timecodes[countedLinks]))); - assertEquals(timecodeStr[countedLinks], text); - countedLinks++; - assertTrue("Contains too many links: " + countedLinks + " > " - + timecodes.length, countedLinks <= timecodes.length); - } - } - assertEquals(timecodes.length, countedLinks); - } - - @Test - public void testIsTimecodeLink() { - assertFalse(Timeline.isTimecodeLink(null)); - assertFalse(Timeline.isTimecodeLink("http://antennapod/timecode/123123")); - assertFalse(Timeline.isTimecodeLink("antennapod://timecode/")); - assertFalse(Timeline.isTimecodeLink("antennapod://123123")); - assertFalse(Timeline.isTimecodeLink("antennapod://timecode/123123a")); - assertTrue(Timeline.isTimecodeLink("antennapod://timecode/123")); - assertTrue(Timeline.isTimecodeLink("antennapod://timecode/1")); - } - - @Test - public void testGetTimecodeLinkTime() { - assertEquals(-1, Timeline.getTimecodeLinkTime(null)); - assertEquals(-1, Timeline.getTimecodeLinkTime("http://timecode/123")); - assertEquals(123, Timeline.getTimecodeLinkTime("antennapod://timecode/123")); - - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java deleted file mode 100644 index b213a5efa..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package de.test.antennapod.util.syndication; - -import androidx.test.platform.app.InstrumentationRegistry; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileOutputStream; -import java.nio.charset.Charset; -import java.util.Map; - -import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for FeedDiscoverer - */ -public class FeedDiscovererTest { - - private FeedDiscoverer fd; - - private File testDir; - - @Before - public void setUp() throws Exception { - fd = new FeedDiscoverer(); - testDir = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getFilesDir(), "FeedDiscovererTest"); - testDir.mkdir(); - assertTrue(testDir.exists()); - } - - @After - public void tearDown() throws Exception { - FileUtils.deleteDirectory(testDir); - } - - private String createTestHtmlString(String rel, String type, String href, String title) { - return String.format("<html><head><title>Test</title><link rel=\"%s\" type=\"%s\" href=\"%s\" title=\"%s\"></head><body></body></html>", - rel, type, href, title); - } - - private String createTestHtmlString(String rel, String type, String href) { - return String.format("<html><head><title>Test</title><link rel=\"%s\" type=\"%s\" href=\"%s\"></head><body></body></html>", - rel, type, href); - } - - private void checkFindUrls(boolean isAlternate, boolean isRss, boolean withTitle, boolean isAbsolute, boolean fromString) throws Exception { - final String title = "Test title"; - final String hrefAbs = "http://example.com/feed"; - final String hrefRel = "/feed"; - final String base = "http://example.com"; - - final String rel = (isAlternate) ? "alternate" : "feed"; - final String type = (isRss) ? "application/rss+xml" : "application/atom+xml"; - final String href = (isAbsolute) ? hrefAbs : hrefRel; - - Map<String, String> res; - String html = (withTitle) ? createTestHtmlString(rel, type, href, title) - : createTestHtmlString(rel, type, href); - if (fromString) { - res = fd.findLinks(html, base); - } else { - File testFile = new File(testDir, "feed"); - FileOutputStream out = new FileOutputStream(testFile); - IOUtils.write(html, out, Charset.forName("UTF-8")); - out.close(); - res = fd.findLinks(testFile, base); - } - - assertNotNull(res); - assertEquals(1, res.size()); - for (String key : res.keySet()) { - assertEquals(hrefAbs, key); - } - assertTrue(res.containsKey(hrefAbs)); - if (withTitle) { - assertEquals(title, res.get(hrefAbs)); - } else { - assertEquals(href, res.get(hrefAbs)); - } - } - - @Test - public void testAlternateRSSWithTitleAbsolute() throws Exception { - checkFindUrls(true, true, true, true, true); - } - - @Test - public void testAlternateRSSWithTitleRelative() throws Exception { - checkFindUrls(true, true, true, false, true); - } - - @Test - public void testAlternateRSSNoTitleAbsolute() throws Exception { - checkFindUrls(true, true, false, true, true); - } - - @Test - public void testAlternateRSSNoTitleRelative() throws Exception { - checkFindUrls(true, true, false, false, true); - } - - @Test - public void testAlternateAtomWithTitleAbsolute() throws Exception { - checkFindUrls(true, false, true, true, true); - } - - @Test - public void testFeedAtomWithTitleAbsolute() throws Exception { - checkFindUrls(false, false, true, true, true); - } - - @Test - public void testAlternateRSSWithTitleAbsoluteFromFile() throws Exception { - checkFindUrls(true, true, true, true, false); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java deleted file mode 100644 index c80e3bbb1..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.test.antennapod.util.syndication.feedgenerator; - -import android.util.Xml; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.OutputStream; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.DateUtils; - -/** - * Creates Atom feeds. See FeedGenerator for more information. - */ -public class AtomGenerator implements FeedGenerator { - - private static final String NS_ATOM = "http://www.w3.org/2005/Atom"; - - private static final long FEATURE_USE_RFC3339LOCAL = 1; - - @Override - public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException { - if (feed == null) throw new IllegalArgumentException("feed = null"); - if (outputStream == null) throw new IllegalArgumentException("outputStream = null"); - if (encoding == null) throw new IllegalArgumentException("encoding = null"); - - XmlSerializer xml = Xml.newSerializer(); - xml.setOutput(outputStream, encoding); - xml.startDocument(encoding, null); - - xml.startTag(null, "feed"); - xml.attribute(null, "xmlns", NS_ATOM); - - // Write Feed data - if (feed.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(feed.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (feed.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(feed.getTitle()); - xml.endTag(null, "title"); - } - if (feed.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", feed.getLink()); - xml.endTag(null, "link"); - } - if (feed.getDescription() != null) { - xml.startTag(null, "subtitle"); - xml.text(feed.getDescription()); - xml.endTag(null, "subtitle"); - } - if (feed.getImageUrl() != null) { - xml.startTag(null, "logo"); - xml.text(feed.getImageUrl()); - xml.endTag(null, "logo"); - } - - if (feed.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), false); - } - - // Write FeedItem data - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - xml.startTag(null, "entry"); - - if (item.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(item.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (item.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(item.getTitle()); - xml.endTag(null, "title"); - } - if (item.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", item.getLink()); - xml.endTag(null, "link"); - } - if (item.getPubDate() != null) { - xml.startTag(null, "published"); - if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) { - xml.text(DateUtils.formatRFC3339Local(item.getPubDate())); - } else { - xml.text(DateUtils.formatRFC3339UTC(item.getPubDate())); - } - xml.endTag(null, "published"); - } - if (item.getDescription() != null) { - xml.startTag(null, "content"); - xml.text(item.getDescription()); - xml.endTag(null, "content"); - } - if (item.getMedia() != null) { - FeedMedia media = item.getMedia(); - xml.startTag(null, "link"); - xml.attribute(null, "rel", "enclosure"); - xml.attribute(null, "href", media.getDownload_url()); - xml.attribute(null, "type", media.getMime_type()); - xml.attribute(null, "length", String.valueOf(media.getSize())); - xml.endTag(null, "link"); - } - - if (item.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), false); - } - - xml.endTag(null, "entry"); - } - } - - writeAdditionalAttributes(xml); - - xml.endTag(null, "feed"); - xml.endDocument(); - } - - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb205b1c3..79fcc430f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,10 +84,37 @@ android:windowSoftInputMode="stateAlwaysHidden" android:launchMode="singleTask" android:label="@string/app_name"> + <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/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" /> + </intent-filter> </activity> <activity android:name=".activity.DownloadAuthenticationActivity" + android:theme="@style/Theme.AntennaPod.Dark.Translucent" android:launchMode="singleInstance"/> <activity @@ -190,13 +217,8 @@ android:name="android.support.PARENT_ACTIVITY" android:value="de.danoeh.antennapod.activity.MainActivity"/> <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:scheme="file"/> - <data android:mimeType="video/*"/> + <action android:name="de.danoeh.antennapod.intents.VIDEO_PLAYER" /> + <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> @@ -292,19 +314,6 @@ </activity> - <activity - android:name=".activity.gpoddernet.GpodnetAuthenticationActivity" - android:configChanges="orientation" - android:label="@string/gpodnet_auth_label"> - <intent-filter> - <action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - <meta-data - android:name="android.support.PARENT_ACTIVITY" - android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> - </activity> - <receiver android:name=".receiver.ConnectivityActionReceiver"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> @@ -333,6 +342,10 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> + + <meta-data + android:name="com.google.android.actions" + android:resource="@xml/actions" /> </application> </manifest> diff --git a/app/src/main/assets/licenses.xml b/app/src/main/assets/licenses.xml index 4f0255182..aa0ad740b 100644 --- a/app/src/main/assets/licenses.xml +++ b/app/src/main/assets/licenses.xml @@ -4,7 +4,7 @@ name="AntennaPod" author="The AntennaPod team" website="https://github.com/AntennaPod/AntennaPod/" - license="MIT" + license="GPL-3.0" licenseText="LICENSE.txt" /> <library name="AntennaPod-AudioPlayer" @@ -91,12 +91,6 @@ license="Apache 2.0" licenseText="LICENSE_APACHE-2.0.txt" /> <library - name="RecyclerView-FlexibleDivider" - author="yqritc" - website="https://github.com/yqritc/RecyclerView-FlexibleDivider" - license="Apache 2.0" - licenseText="LICENSE_APACHE-2.0.txt" /> - <library name="RxAndroid" author="ReactiveX" website="https://github.com/ReactiveX/RxAndroid" diff --git a/app/src/main/assets/special_thanks.csv b/app/src/main/assets/special_thanks.csv index 348e3208e..ab44bd7f4 100644 --- a/app/src/main/assets/special_thanks.csv +++ b/app/src/main/assets/special_thanks.csv @@ -1,3 +1,4 @@ 221 Pixels;Logo design;https://avatars2.githubusercontent.com/u/58243143?s=60&v=4 +Anxhelo Lushka;Website design;https://avatars2.githubusercontent.com/u/25004151?s=60&v=4 ByteHamster;Forum admin;https://avatars2.githubusercontent.com/u/5811634?s=60&v=4 Keunes;Communications;https://avatars2.githubusercontent.com/u/11229646?s=60&v=4 diff --git a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java index bff11fa5e..721291597 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java @@ -11,10 +11,15 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; import com.google.android.material.snackbar.Snackbar; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; +import android.view.Menu; +import android.view.MenuItem; import android.widget.TextView; @@ -67,42 +72,65 @@ public class BugReportActivity extends AppCompatActivity { clipboard.setPrimaryClip(clip); Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show(); }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.bug_report_options, menu); + return super.onCreateOptionsMenu(menu); + } - findViewById(R.id.btn_export_logcat).setOnClickListener(v -> { + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.export_logcat) { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this); + alertBuilder.setMessage(R.string.confirm_export_log_dialog_message); + alertBuilder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + exportLog(); + dialog.dismiss(); + }); + alertBuilder.setNegativeButton(R.string.cancel_label, null); + alertBuilder.show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void exportLog() { + try { + File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt"); + filename.createNewFile(); + String cmd = "logcat -d -f " + filename.getAbsolutePath(); + Runtime.getRuntime().exec(cmd); + //share file try { - File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt"); - filename.createNewFile(); - String cmd = "logcat -d -f " + filename.getAbsolutePath(); - Runtime.getRuntime().exec(cmd); - //share file - try { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType("text/*"); - String authString = getString(de.danoeh.antennapod.core.R.string.provider_authority); - Uri fileUri = FileProvider.getUriForFile(this, authString, filename); - i.putExtra(Intent.EXTRA_STREAM, fileUri); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - PackageManager pm = getPackageManager(); - List<ResolveInfo> resInfos = pm.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfos) { - String packageName = resolveInfo.activityInfo.packageName; - grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/*"); + String authString = getString(de.danoeh.antennapod.core.R.string.provider_authority); + Uri fileUri = FileProvider.getUriForFile(this, authString, filename); + i.putExtra(Intent.EXTRA_STREAM, fileUri); + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + PackageManager pm = getPackageManager(); + List<ResolveInfo> resInfos = pm.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfos) { + String packageName = resolveInfo.activityInfo.packageName; + grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } - String chooserTitle = getString(de.danoeh.antennapod.core.R.string.share_file_label); - startActivity(Intent.createChooser(i, chooserTitle)); - } catch (Exception e) { - e.printStackTrace(); - int strResId = R.string.log_file_share_exception; - Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG) - .show(); } - } catch (IOException e) { + String chooserTitle = getString(de.danoeh.antennapod.core.R.string.share_file_label); + startActivity(Intent.createChooser(i, chooserTitle)); + } catch (Exception e) { e.printStackTrace(); - Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show(); + int strResId = R.string.log_file_share_exception; + Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG) + .show(); } - }); + } catch (IOException e) { + e.printStackTrace(); + Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show(); + } } + } 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 1d3d9bf11..0f1d38db6 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -1,96 +1,76 @@ package de.danoeh.antennapod.activity; -import android.app.Activity; -import android.content.Intent; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; +import android.text.TextUtils; import androidx.appcompat.app.AppCompatActivity; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import org.apache.commons.lang3.Validate; - import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.Validate; + /** * Shows a username and a password text field. * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value. - * Other arguments are optional. - * The activity's result will be the same DownloadRequest with the entered username and password. */ public class DownloadAuthenticationActivity extends AppCompatActivity { /** - * The download request object that contains information about the resource that requires a username and a password + * 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"; - /** - * True if the request should be sent to the DownloadRequester when this activity is finished, false otherwise. - * The default value is false. - */ - public static final String ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL = "send_to_downloadrequester"; - - private static final String RESULT_REQUEST = "request"; - - private EditText etxtUsername; - private EditText etxtPassword; @Override protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getNoTitleTheme()); + setTheme(UserPreferences.getTranslucentTheme()); super.onCreate(savedInstanceState); - setContentView(R.layout.download_authentication_activity); - TextView txtvDescription = findViewById(R.id.txtvDescription); - etxtUsername = findViewById(R.id.etxtUsername); - etxtPassword = findViewById(R.id.etxtPassword); - Button butConfirm = findViewById(R.id.butConfirm); - Button butCancel = findViewById(R.id.butCancel); - Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing"); DownloadRequest request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST); - boolean sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false); - - String newDescription = txtvDescription.getText() + ":\n\n" + request.getTitle(); - txtvDescription.setText(newDescription); - - if (savedInstanceState != null) { - etxtUsername.setText(savedInstanceState.getString("username")); - etxtPassword.setText(savedInstanceState.getString("password")); - } - butConfirm.setOnClickListener(v -> { - String username = etxtUsername.getText().toString(); - String password = etxtPassword.getText().toString(); - request.setUsername(username); - request.setPassword(password); - Intent result = new Intent(); - result.putExtra(RESULT_REQUEST, request); - setResult(Activity.RESULT_OK, result); - - if (sendToDownloadRequester) { - DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + new AuthenticationDialog(this, R.string.authentication_label, true, "", "") { + @Override + protected void onConfirmed(String username, String password) { + Completable.fromAction( + () -> { + request.setUsername(username); + request.setPassword(password); + + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + long mediaId = request.getFeedfileId(); + FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media != null) { + FeedPreferences preferences = media.getItem().getFeed().getPreferences(); + if (TextUtils.isEmpty(preferences.getPassword()) + || TextUtils.isEmpty(preferences.getUsername())) { + preferences.setUsername(username); + preferences.setPassword(password); + DBWriter.setFeedPreferences(preferences); + } + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + finish(); + }); } - finish(); - }); - - butCancel.setOnClickListener(v -> { - setResult(Activity.RESULT_CANCELED); - finish(); - }); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("username", etxtUsername.getText().toString()); - outState.putString("password", etxtPassword.getText().toString()); + @Override + protected void onCancelled() { + finish(); + } + }.show(); } } 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 d1716e009..69f5fb264 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.media.AudioManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -40,7 +41,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; @@ -51,9 +52,11 @@ import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.preferences.PreferenceUpgrader; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; @@ -75,7 +78,6 @@ public class MainActivity extends CastEnabledActivity { public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; public static final String EXTRA_FEED_ID = "fragment_feed_id"; - public static final String EXTRA_OPEN_PLAYER = "open_player"; public static final String EXTRA_REFRESH_ON_START = "refresh_on_start"; public static final String EXTRA_STARTED_FROM_SEARCH = "started_from_search"; public static final String KEY_GENERATED_VIEW_ID = "generated_view_id"; @@ -508,9 +510,11 @@ public class MainActivity extends CastEnabledActivity { } } sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else if (intent.getBooleanExtra(EXTRA_OPEN_PLAYER, false)) { + } 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()); } // to avoid handling the intent twice when the configuration changes setIntent(new Intent(MainActivity.this, MainActivity.class)); @@ -520,6 +524,7 @@ public class MainActivity extends CastEnabledActivity { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); + handleNavIntent(); } public Snackbar showSnackbarAbovePlayer(CharSequence text, int duration) { @@ -540,6 +545,59 @@ public class MainActivity extends CastEnabledActivity { return showSnackbarAbovePlayer(getResources().getText(text), duration); } + /** + * Handles the deep link incoming via App Actions. + * Performs an in-app search or opens the relevant feature of the app + * depending on the query. + * + * @param uri incoming deep link + */ + private void handleDeeplink(Uri uri) { + if (uri == null || uri.getPath() == null) { + return; + } + Log.d(TAG, "Handling deeplink: " + uri.toString()); + switch (uri.getPath()) { + case "/deeplink/search": + String query = uri.getQueryParameter("query"); + if (query == null) { + return; + } + + this.loadChildFragment(SearchFragment.newInstance(query)); + break; + case "/deeplink/main": + String feature = uri.getQueryParameter("page"); + if (feature == null) { + return; + } + switch (feature) { + case "DOWNLOADS": + loadFragment(DownloadsFragment.TAG, null); + break; + case "HISTORY": + loadFragment(PlaybackHistoryFragment.TAG, null); + break; + case "EPISODES": + loadFragment(EpisodesFragment.TAG, null); + break; + case "QUEUE": + loadFragment(QueueFragment.TAG, null); + break; + case "SUBSCRIPTIONS": + loadFragment(SubscriptionFragment.TAG, null); + break; + default: + showSnackbarAbovePlayer(getString(R.string.app_action_not_found, feature), + Snackbar.LENGTH_LONG); + return; + } + break; + default: + break; + } + } + //Hardware keyboard support @Override public boolean onKeyUp(int keyCode, KeyEvent event) { @@ -592,5 +650,4 @@ public class MainActivity extends CastEnabledActivity { } return super.onKeyUp(keyCode, event); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index b03d1e5cd..76359ddc0 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -1,10 +1,8 @@ package de.danoeh.antennapod.activity; -import android.Manifest; import android.annotation.TargetApi; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.graphics.PixelFormat; import android.os.Build; import android.os.Bundle; @@ -17,7 +15,6 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; -import android.widget.Toast; import com.bumptech.glide.Glide; @@ -28,18 +25,16 @@ import org.greenrobot.eventbus.ThreadMode; import java.text.NumberFormat; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; +import androidx.cardview.widget.CardView; import androidx.core.app.ActivityOptionsCompat; -import androidx.core.content.ContextCompat; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; @@ -51,11 +46,9 @@ import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.dialog.PlaybackControlsDialog; import de.danoeh.antennapod.dialog.ShareDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog; @@ -65,7 +58,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - /** * Provides general features which are both needed for playing audio and video * files. @@ -73,9 +65,6 @@ import io.reactivex.schedulers.Schedulers; public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener { private static final String TAG = "MediaplayerActivity"; private static final String PREFS = "MediaPlayerActivityPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; - private static final int REQUEST_CODE_STORAGE_PLAY_VIDEO = 42; - private static final int REQUEST_CODE_STORAGE_PLAY_AUDIO = 43; PlaybackController controller; @@ -88,6 +77,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private ImageButton butFF; private TextView txtvFF; private ImageButton butSkip; + private CardView cardViewSeek; + private TextView txtvSeek; private boolean showTimeLeft = false; @@ -473,8 +464,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if(controller == null || controller.getMedia() == null) { return false; } - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); onPositionObserverUpdate(); checkFavorite(); updatePlaybackSpeedButton(); @@ -493,9 +483,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements setContentView(getContentViewResourceId()); sbPosition = findViewById(R.id.sbPosition); txtvPosition = findViewById(R.id.txtvPosition); + cardViewSeek = findViewById(R.id.cardViewSeek); + txtvSeek = findViewById(R.id.txtvSeek); SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); Log.d("timeleft", showTimeLeft ? "true" : "false"); txtvLength = findViewById(R.id.txtvLength); if (txtvLength != null) { @@ -519,9 +511,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } txtvLength.setText(length); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft); - editor.apply(); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); Log.d("timeleft on click", showTimeLeft ? "true" : "false"); }); } @@ -619,21 +609,21 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } if (fromUser) { prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - txtvLength.setText("-" + Converter.getDurationStringLong(timeLeft)); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -641,6 +631,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (controller != null) { controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } private void checkFavorite() { @@ -664,50 +661,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - void playExternalMedia(Intent intent, MediaType type) { - if (intent == null || intent.getData() == null) { - return; - } - if (Build.VERSION.SDK_INT >= 23 - && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - - int code = REQUEST_CODE_STORAGE_PLAY_AUDIO; - if (type == MediaType.VIDEO) { - code = REQUEST_CODE_STORAGE_PLAY_VIDEO; - } - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, code); - return; - } - - Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), type); - - new PlaybackServiceStarter(this, media) - .callEvenIfRunning(true) - .startWhenPrepared(true) - .shouldStream(false) - .prepareImmediately(true) - .start(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CODE_STORAGE_PLAY_AUDIO) { - playExternalMedia(getIntent(), MediaType.AUDIO); - } else if (requestCode == REQUEST_CODE_STORAGE_PLAY_VIDEO) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } - } else { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - } - @Nullable private static FeedItem getFeedItem(@Nullable Playable playable) { if (playable instanceof FeedMedia) { 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 3f58e4a92..f53c629b9 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -4,6 +4,7 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.LightingColorFilter; import android.os.Build; import android.os.Bundle; @@ -87,6 +88,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity { // Optional argument: specify a title for the actionbar. private static final int RESULT_ERROR = 2; private static final String TAG = "OnlineFeedViewActivity"; + private static final String PREFS = "OnlineFeedViewActivityPreferences"; + private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"; + private volatile List<Feed> feeds; private Feed feed; private String selectedDownloadUrl; @@ -445,6 +449,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity { IntentUtils.sendLocalBroadcast(this, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); }); + if (UserPreferences.isEnableAutodownload()) { + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)); + } + final int MAX_LINES_COLLAPSED = 10; description.setMaxLines(MAX_LINES_COLLAPSED); description.setOnClickListener(v -> { @@ -509,11 +518,28 @@ public class OnlineFeedViewActivity extends AppCompatActivity { viewBinding.subscribeButton.setEnabled(true); viewBinding.subscribeButton.setText(R.string.open_podcast); if (didPressSubscribe) { + didPressSubscribe = false; + if (UserPreferences.isEnableAutodownload()) { + boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked(); + + Feed feed1 = DBReader.getFeed(getFeedId(feed)); + FeedPreferences feedPreferences = feed1.getPreferences(); + feedPreferences.setAutoDownload(autoDownload); + feed1.savePreferences(); + + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload); + editor.apply(); + } openFeed(); } } else { viewBinding.subscribeButton.setEnabled(true); viewBinding.subscribeButton.setText(R.string.subscribe_label); + if (UserPreferences.isEnableAutodownload()) { + viewBinding.autoDownloadCheckBox.setVisibility(View.VISIBLE); + } } } } 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 10292b892..d4e9ee5d9 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java @@ -17,9 +17,8 @@ import de.danoeh.antennapod.asynctask.OpmlFeedQueuer; import de.danoeh.antennapod.asynctask.OpmlImportWorker; import de.danoeh.antennapod.core.export.opml.OpmlElement; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.LangUtils; + import org.apache.commons.io.ByteOrderMark; -import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.ArrayUtils; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java index d85235cf9..f0c76d545 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java @@ -13,9 +13,9 @@ import android.widget.ProgressBar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.error.CrashReportWriter; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; /** @@ -44,13 +44,15 @@ public class SplashActivity extends AppCompatActivity { }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - Intent intent = new Intent(SplashActivity.this, MainActivity.class); - startActivity(intent); - overridePendingTransition(0, 0); - finish(); - }, error -> { + .subscribe( + () -> { + Intent intent = new Intent(SplashActivity.this, MainActivity.class); + startActivity(intent); + overridePendingTransition(0, 0); + finish(); + }, error -> { error.printStackTrace(); + CrashReportWriter.write(error); Toast.makeText(this, error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); finish(); }); 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 9ab1755f5..749681d4a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -15,10 +15,8 @@ import android.view.animation.ScaleAnimation; import android.widget.EditText; import android.widget.ImageView; -import androidx.appcompat.view.menu.ActionMenuItem; import androidx.core.view.WindowCompat; import androidx.appcompat.app.ActionBar; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.Menu; @@ -38,12 +36,12 @@ import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.AspectRatioVideoView; /** @@ -89,9 +87,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onResume() { super.onResume(); - if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } else if (PlaybackService.isCasting()) { + if (PlaybackService.isCasting()) { Intent intent = PlaybackService.getPlayerActivityIntent(this); if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { destroyingDueToReload = true; @@ -348,7 +344,7 @@ public class VideoplayerActivity extends MediaplayerActivity { Log.d(TAG, "ReloadNotification received, switching to Castplayer now"); destroyingDueToReload = true; finish(); - startActivity(new Intent(this, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); + new MainActivityStarter(this).withOpenPlayer().start(); } } 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 474b96c38..3020aba43 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java @@ -2,34 +2,34 @@ package de.danoeh.antennapod.activity; import android.Manifest; import android.app.WallpaperManager; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.widget.ImageView; -import android.widget.RemoteViews; -import androidx.appcompat.app.AppCompatActivity; - import android.appwidget.AppWidgetManager; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.view.View; -import android.widget.RelativeLayout; +import android.widget.CheckBox; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; - +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.PlayerWidgetJobService; +import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService; public class WidgetConfigActivity extends AppCompatActivity { private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; private SeekBar opacitySeekBar; private TextView opacityTextView; - private RelativeLayout widgetPreview; + private View widgetPreview; + private CheckBox ckRewind; + private CheckBox ckFastForward; + private CheckBox ckSkip; @Override protected void onCreate(Bundle savedInstanceState) { @@ -74,6 +74,32 @@ public class WidgetConfigActivity extends AppCompatActivity { } }); + + widgetPreview.findViewById(R.id.txtNoPlaying).setVisibility(View.GONE); + TextView title = widgetPreview.findViewById(R.id.txtvTitle); + title.setVisibility(View.VISIBLE); + title.setText(R.string.app_name); + TextView progress = widgetPreview.findViewById(R.id.txtvProgress); + progress.setVisibility(View.VISIBLE); + progress.setText(R.string.position_default_label); + + ckRewind = findViewById(R.id.ckRewind); + ckRewind.setOnClickListener(v -> displayPreviewPanel()); + ckFastForward = findViewById(R.id.ckFastForward); + ckFastForward.setOnClickListener(v -> displayPreviewPanel()); + ckSkip = findViewById(R.id.ckSkip); + ckSkip.setOnClickListener(v -> displayPreviewPanel()); + } + + private void displayPreviewPanel() { + boolean showExtendedPreview = ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked(); + widgetPreview.findViewById(R.id.extendedButtonsContainer) + .setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE); + widgetPreview.findViewById(R.id.butFastForward) + .setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE); } private void displayDeviceBackground() { @@ -92,13 +118,16 @@ public class WidgetConfigActivity extends AppCompatActivity { SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor); + editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked()); editor.apply(); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, resultValue); finish(); - PlayerWidgetJobService.updateWidget(this); + WidgetUpdaterJobService.performBackgroundUpdate(this); } private int getColorWithAlpha(int color, int opacity) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java deleted file mode 100644 index cfd6ec702..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -package de.danoeh.antennapod.activity.gpoddernet; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.ViewFlipper; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; - -/** - * Guides the user through the authentication process - * Step 1: Request username and password from user - * Step 2: Choose device from a list of available devices or create a new one - * Step 3: Choose from a list of actions - */ -public class GpodnetAuthenticationActivity extends AppCompatActivity { - private static final String TAG = "GpodnetAuthActivity"; - - private ViewFlipper viewFlipper; - - private static final int STEP_DEFAULT = -1; - private static final int STEP_LOGIN = 0; - private static final int STEP_DEVICE = 1; - private static final int STEP_FINISH = 2; - - private int currentStep = -1; - - private GpodnetService service; - private volatile String username; - private volatile String password; - private volatile GpodnetDevice selectedDevice; - - private View[] views; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.gpodnetauth_activity); - service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); - - viewFlipper = findViewById(R.id.viewflipper); - LayoutInflater inflater = (LayoutInflater) - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - views = new View[]{ - inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) - }; - for (View view : views) { - viewFlipper.addView(view); - } - advance(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void setupLoginView(View view) { - final EditText username = view.findViewById(R.id.etxtUsername); - final EditText password = view.findViewById(R.id.etxtPassword); - final Button login = view.findViewById(R.id.butLogin); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); - - password.setOnEditorActionListener((v, actionID, event) -> - actionID == EditorInfo.IME_ACTION_GO && login.performClick()); - - login.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - final String usernameStr = username.getText().toString(); - final String passwordStr = password.getText().toString(); - - if (usernameHasUnwantedChars(usernameStr)) { - txtvError.setText(R.string.gpodnetsync_username_characters_error); - txtvError.setVisibility(View.VISIBLE); - return; - } - if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); - AsyncTask<GpodnetService, Void, Void> authTask = new AsyncTask<GpodnetService, Void, Void>() { - - volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - login.setEnabled(false); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - // hide the keyboard - InputMethodManager inputManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(login.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); - - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - login.setEnabled(true); - progressBar.setVisibility(View.GONE); - - if (exception == null) { - advance(); - } else { - txtvError.setText(exception.getCause().getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected Void doInBackground(GpodnetService... params) { - try { - params[0].authenticate(usernameStr, passwordStr); - GpodnetAuthenticationActivity.this.username = usernameStr; - GpodnetAuthenticationActivity.this.password = passwordStr; - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }; - authTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, service); - } - }); - } - - private void setupDeviceView(View view) { - final EditText deviceID = view.findViewById(R.id.etxtDeviceID); - final EditText caption = view.findViewById(R.id.etxtCaption); - final Button createNewDevice = view.findViewById(R.id.butCreateNewDevice); - final Button chooseDevice = view.findViewById(R.id.butChooseExistingDevice); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); - final Spinner spinnerDevices = view.findViewById(R.id.spinnerChooseDevice); - - - // load device list - final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<>(); - new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - chooseDevice.setEnabled(false); - spinnerDevices.setEnabled(false); - createNewDevice.setEnabled(false); - } - - @Override - protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) { - super.onPostExecute(gpodnetDevices); - if (gpodnetDevices != null) { - List<String> deviceNames = new ArrayList<>(); - for (GpodnetDevice device : gpodnetDevices) { - deviceNames.add(device.getCaption()); - } - spinnerDevices.setAdapter(new ArrayAdapter<>(GpodnetAuthenticationActivity.this, - android.R.layout.simple_spinner_dropdown_item, deviceNames)); - spinnerDevices.setEnabled(true); - if (!deviceNames.isEmpty()) { - chooseDevice.setEnabled(true); - } - devices.set(gpodnetDevices); - deviceID.setText(generateDeviceID(gpodnetDevices)); - createNewDevice.setEnabled(true); - } - } - - @Override - protected List<GpodnetDevice> doInBackground(GpodnetService... params) { - try { - return params[0].getDevices(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - return null; - } - } - }.execute(service); - - - createNewDevice.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkDeviceIDText(deviceID, caption, txtvError, devices.get())) { - final String deviceStr = deviceID.getText().toString(); - final String captionStr = caption.getText().toString(); - - new AsyncTask<GpodnetService, Void, GpodnetDevice>() { - - private volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - createNewDevice.setEnabled(false); - chooseDevice.setEnabled(false); - progBarCreateDevice.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - } - - @Override - protected void onPostExecute(GpodnetDevice result) { - super.onPostExecute(result); - createNewDevice.setEnabled(true); - chooseDevice.setEnabled(true); - progBarCreateDevice.setVisibility(View.GONE); - if (exception == null) { - selectedDevice = result; - advance(); - } else { - txtvError.setText(exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected GpodnetDevice doInBackground(GpodnetService... params) { - try { - params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); - return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }.execute(service); - } - } - }); - - chooseDevice.setOnClickListener(v -> { - final int position = spinnerDevices.getSelectedItemPosition(); - if (position != AdapterView.INVALID_POSITION) { - selectedDevice = devices.get().get(position); - advance(); - } - }); - } - - - private String generateDeviceID(List<GpodnetDevice> gpodnetDevices) { - // devices names must be of a certain form: - // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices - // This is more restrictive than needed, but I think it makes for more readable names. - String baseId = Build.MODEL.replaceAll("\\W", ""); - String id = baseId; - int num = 0; - - while (isDeviceWithIdInList(id, gpodnetDevices)) { - id = baseId + "_" + num; - num++; - } - - return id; - } - - private boolean isDeviceWithIdInList(String id, List<GpodnetDevice> gpodnetDevices) { - if (gpodnetDevices == null) { - return false; - } - for (GpodnetDevice device : gpodnetDevices) { - if (device.getId().equals(id)) { - return true; - } - } - return false; - } - - private boolean checkDeviceIDText(EditText deviceID, EditText caption, TextView txtvError, List<GpodnetDevice> devices) { - String text = deviceID.getText().toString(); - if (text.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else if (caption.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_caption_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else { - if (devices != null) { - if (isDeviceWithIdInList(text, devices)) { - txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); - txtvError.setVisibility(View.VISIBLE); - return false; - } - txtvError.setVisibility(View.GONE); - return true; - } - return true; - } - - } - - private void setupFinishView(View view) { - final Button sync = view.findViewById(R.id.butSyncNow); - final Button back = view.findViewById(R.id.butGoMainscreen); - - sync.setOnClickListener(v -> { - finish(); - SyncService.sync(getApplicationContext()); - }); - back.setOnClickListener(v -> { - Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - }); - } - - private void writeLoginCredentials() { - if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials"); - GpodnetPreferences.setUsername(username); - GpodnetPreferences.setPassword(password); - GpodnetPreferences.setDeviceID(selectedDevice.getId()); - } - - private void advance() { - if (currentStep < STEP_FINISH) { - - View view = views[currentStep + 1]; - if (currentStep == STEP_DEFAULT) { - setupLoginView(view); - } else if (currentStep == STEP_LOGIN) { - if (username == null || password == null) { - throw new IllegalStateException("Username and password must not be null here"); - } else { - setupDeviceView(view); - } - } else if (currentStep == STEP_DEVICE) { - if (selectedDevice == null) { - throw new IllegalStateException("Device must not be null here"); - } else { - writeLoginCredentials(); - setupFinishView(view); - } - } - if (currentStep != STEP_DEFAULT) { - viewFlipper.showNext(); - } - currentStep++; - } else { - finish(); - } - } - - private boolean usernameHasUnwantedChars(String username) { - Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); - Matcher containsUnwantedChars = special.matcher(username); - return containsUnwantedChars.find(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java index df81156b5..a267938d3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java @@ -20,14 +20,16 @@ import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder> { private Playable media; private final Callback callback; private final Context context; private int currentChapterIndex = -1; + private long currentChapterPosition = -1; private boolean hasImages = false; public ChaptersListAdapter(Context context, Callback callback) { @@ -48,7 +50,6 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte notifyDataSetChanged(); } - @Override public void onBindViewHolder(@NonNull ChapterHolder holder, int position) { Chapter sc = getItem(position); @@ -83,8 +84,14 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte if (position == currentChapterIndex) { int playingBackGroundColor = ThemeUtils.getColorFromAttr(context, R.attr.currently_playing_background); holder.itemView.setBackgroundColor(playingBackGroundColor); + float progress = ((float) (currentChapterPosition - sc.getStart())) / duration; + progress = Math.max(progress, CircularProgressBar.MINIMUM_PERCENTAGE); + progress = Math.min(progress, CircularProgressBar.MAXIMUM_PERCENTAGE); + holder.progressBar.setPercentage(progress, position); + holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.av_replay)); } else { holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); + holder.progressBar.setPercentage(0, null); } if (hasImages) { @@ -136,6 +143,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte final ImageView image; final View secondaryActionButton; final ImageView secondaryActionIcon; + final CircularProgressBar progressBar; public ChapterHolder(@NonNull View itemView) { super(itemView); @@ -146,14 +154,23 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte duration = itemView.findViewById(R.id.txtvDuration); secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + progressBar = itemView.findViewById(R.id.secondaryActionProgress); } } public void notifyChapterChanged(int newChapterIndex) { currentChapterIndex = newChapterIndex; + currentChapterPosition = getItem(newChapterIndex).getStart(); notifyDataSetChanged(); } + public void notifyTimeChanged(long timeMs) { + currentChapterPosition = timeMs; + // Passing an argument prevents flickering. + // See EpisodeItemListAdapter.notifyItemChangedCompat. + notifyItemChanged(currentChapterIndex, "foo"); + } + private boolean ignoreChapter(Chapter c) { return media.getDuration() > 0 && media.getDuration() < c.getStart(); } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 7d195a9ad..d6801e1b5 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -1,23 +1,15 @@ package de.danoeh.antennapod.adapter; import android.app.Activity; -import android.content.Context; -import android.os.Build; -import android.text.Layout; import android.text.format.DateUtils; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.TextView; import android.widget.Toast; import androidx.core.content.ContextCompat; -import com.joanzapata.iconify.widget.IconButton; -import com.joanzapata.iconify.widget.IconTextView; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; @@ -28,7 +20,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder; /** diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index 540cd054c..9d63d8ad7 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -15,8 +15,8 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class DownloadlistAdapter extends BaseAdapter { @@ -65,6 +65,7 @@ public class DownloadlistAdapter extends BaseAdapter { holder.title.setText(request.getTitle()); holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.navigation_cancel)); + holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); holder.secondaryActionButton.setTag(downloader); holder.secondaryActionButton.setOnClickListener(butSecondaryListener); holder.secondaryActionProgress.setPercentage(0, request); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 6bfd34d5c..8cb0fd30a 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -13,8 +13,6 @@ import androidx.annotation.Nullable; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.playback.RemoteMedia; import de.danoeh.antennapod.core.feed.FeedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java index 2e5ba31c9..dbb9ce0d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.view.SquareImageView; +import de.danoeh.antennapod.ui.common.SquareImageView; import java.lang.ref.WeakReference; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index 92ed7b052..f8507ba74 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -75,7 +75,7 @@ public class NavListAdapter extends BaseAdapter } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)) { + if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) { loadItems(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index 7ce086694..01712ea29 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -6,7 +6,6 @@ import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MotionEvent; import android.view.View; -import androidx.core.view.MotionEventCompat; import androidx.recyclerview.widget.ItemTouchHelper; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java index 8c294a9c9..919a4e217 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java @@ -13,8 +13,6 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; -import com.bumptech.glide.Glide; - import java.lang.ref.WeakReference; import java.text.NumberFormat; import java.util.Locale; @@ -23,7 +21,6 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.LocalFeedUpdater; -import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import jp.shts.android.library.TriangleLabelView; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java index 31dfe15da..78ea3b93f 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java @@ -7,11 +7,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.preferences.UsageStatistics; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; -import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; public class PlayLocalActionButton extends ItemActionButton { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java index 7b8659968..e45280eed 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.view.View; import androidx.annotation.AttrRes; import androidx.annotation.StringRes; diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java index 3ac05e842..906d50c61 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java @@ -9,10 +9,10 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.charset.Charset; import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.LangUtils; import io.reactivex.Observable; /** @@ -44,7 +44,7 @@ public class DocumentFileExportWorker { if (outputStream == null) { throw new IOException(); } - writer = new OutputStreamWriter(outputStream, LangUtils.UTF_8); + writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8")); exportWriter.writeDocument(DBReader.getFeedList(), writer, context); subscriber.onNext(output); } catch (IOException e) { diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java index f81a52402..0930b59eb 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java @@ -8,11 +8,11 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.nio.charset.Charset; import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.LangUtils; import io.reactivex.Observable; /** @@ -47,7 +47,7 @@ public class ExportWorker { return Observable.create(subscriber -> { OutputStreamWriter writer = null; try { - writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8); + writer = new OutputStreamWriter(new FileOutputStream(output), Charset.forName("UTF-8")); exportWriter.writeDocument(DBReader.getFeedList(), writer, context); subscriber.onNext(output); } catch (IOException e) { diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java index 6e584d34f..a45eb5199 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -8,14 +8,13 @@ import de.danoeh.antennapod.core.ClientConfig; */ class ClientConfigurator { - private ClientConfigurator(){} + private ClientConfigurator() { + } static { ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME; ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); - ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); - ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl(); ClientConfig.castCallbacks = new CastCallbackImpl(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java deleted file mode 100644 index c3f7ae9c8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.config; - -import de.danoeh.antennapod.core.DBTasksCallbacks; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.APDownloadAlgorithm; -import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; - -public class DBTasksCallbacksImpl implements DBTasksCallbacks { - - @Override - public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm() { - return new APDownloadAlgorithm(); - } - - @Override - public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm() { - return UserPreferences.getEpisodeCleanupAlgorithm(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java index 71442f50b..f782308d1 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -30,8 +30,8 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { @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); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); return PendingIntent.getActivity(context.getApplicationContext(), R.id.pending_intent_download_service_auth, activityIntent, PendingIntent.FLAG_ONE_SHOT); } @@ -54,9 +54,4 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent, PendingIntent.FLAG_UPDATE_CURRENT); } - - @Override - public boolean shouldCreateReport() { - return true; - } } diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java deleted file mode 100644 index 44f18e9da..000000000 --- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.config; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.core.PlaybackServiceCallbacks; -import de.danoeh.antennapod.core.feed.MediaType; - -public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { - @Override - public Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback) { - if (mediaType == MediaType.AUDIO || remotePlayback) { - return new Intent(context, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true); - } else { - Intent i = new Intent(context, VideoplayerActivity.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - } - return i; - } - } - - @Override - public boolean useQueue() { - return true; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java index 39d321f18..d7b2dc536 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java @@ -1,37 +1,50 @@ package de.danoeh.antennapod.dialog; import android.content.Context; -import android.view.View; -import android.widget.EditText; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.LayoutInflater; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.AuthenticationDialogBinding; /** * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. */ public abstract class AuthenticationDialog extends AlertDialog.Builder { + boolean passwordHidden = true; public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, String usernameInitialValue, String passwordInitialValue) { super(context); setTitle(titleRes); - View rootView = View.inflate(context, R.layout.authentication_dialog, null); - setView(rootView); + AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context)); + setView(viewBinding.getRoot()); - final EditText etxtUsername = rootView.findViewById(R.id.etxtUsername); - final EditText etxtPassword = rootView.findViewById(R.id.etxtPassword); - - etxtUsername.setEnabled(enableUsernameField); + viewBinding.usernameEditText.setEnabled(enableUsernameField); if (usernameInitialValue != null) { - etxtUsername.setText(usernameInitialValue); + viewBinding.usernameEditText.setText(usernameInitialValue); } if (passwordInitialValue != null) { - etxtPassword.setText(passwordInitialValue); + viewBinding.passwordEditText.setText(passwordInitialValue); } + viewBinding.showPasswordButton.setOnClickListener(v -> { + if (passwordHidden) { + viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(1.0f); + } else { + viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(0.6f); + } + passwordHidden = !passwordHidden; + }); + setOnCancelListener(dialog -> onCancelled()); + setOnDismissListener(dialog -> onCancelled()); setNegativeButton(R.string.cancel_label, null); setPositiveButton(R.string.confirm_label, (dialog, which) - -> onConfirmed(etxtUsername.getText().toString(), etxtPassword.getText().toString())); + -> onConfirmed(viewBinding.usernameEditText.getText().toString(), + viewBinding.passwordEditText.getText().toString())); } protected void onCancelled() { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index 5aee3f2be..e1e8f1c2e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -28,7 +28,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.SortOrder; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import java.util.ArrayList; import java.util.Arrays; @@ -436,13 +436,18 @@ public class EpisodesApplyActionFragment extends Fragment implements Toolbar.OnM } private void deleteChecked() { + int countHasMedia = 0; + int countNoMedia = 0; for (long id : checkedIds.toArray()) { FeedItem episode = idMap.get(id); - if (episode.hasMedia()) { + if (episode.hasMedia() && episode.getMedia().isDownloaded()) { + countHasMedia++; DBWriter.deleteFeedMediaOfItem(getActivity(), episode.getMedia().getId()); + } else { + countNoMedia++; } } - close(R.plurals.deleted_episode_batch_label, checkedIds.size()); + closeMore(R.plurals.deleted_multi_episode_batch_label, countNoMedia, countHasMedia); } private void close(@PluralsRes int msgId, int numItems) { @@ -451,4 +456,12 @@ public class EpisodesApplyActionFragment extends Fragment implements Toolbar.OnM getActivity().getSupportFragmentManager().popBackStack(); } + private void closeMore(@PluralsRes int msgId, int countNoMedia, int countHasMedia) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + getResources().getQuantityString(msgId, + (countHasMedia + countNoMedia), + (countHasMedia + countNoMedia), countHasMedia), + Snackbar.LENGTH_LONG); + getActivity().getSupportFragmentManager().popBackStack(); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java index 80df87891..779248e2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java @@ -16,7 +16,7 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedItemFilterGroup; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public abstract class FilterDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java deleted file mode 100644 index 8119dffcb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import androidx.appcompat.app.AlertDialog; -import android.text.Editable; -import android.text.InputType; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.LinearLayout; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; - -/** - * Creates a dialog that lets the user change the hostname for the gpodder.net service. - */ -public class GpodnetSetHostnameDialog { - - private GpodnetSetHostnameDialog(){} - - private static final String TAG = "GpodnetSetHostnameDialog"; - - public static AlertDialog createDialog(final Context context) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - final EditText et = new EditText(context); - et.setText(GpodnetPreferences.getHostname()); - et.setInputType(InputType.TYPE_TEXT_VARIATION_URI); - dialog.setTitle(R.string.pref_gpodnet_sethostname_title) - .setView(setupContentView(context, et)) - .setPositiveButton(R.string.confirm_label, (dialog1, which) -> { - final Editable e = et.getText(); - if (e != null) { - GpodnetPreferences.setHostname(e.toString()); - } - dialog1.dismiss(); - }) - .setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()) - .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, (dialog1, which) -> { - GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); - dialog1.dismiss(); - }) - .setCancelable(true); - return dialog.show(); - } - - private static View setupContentView(Context context, EditText et) { - LinearLayout ll = new LinearLayout(context); - ll.addView(et); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams(); - if (params != null) { - params.setMargins(8, 8, 8, 8); - params.width = ViewGroup.LayoutParams.MATCH_PARENT; - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } - return ll; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java index 699c6f492..a4e49ff96 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java @@ -1,16 +1,15 @@ package de.danoeh.antennapod.dialog; import android.app.Activity; -import android.text.InputType; import java.lang.ref.WeakReference; import android.view.View; -import android.widget.EditText; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; public class RenameFeedDialog { @@ -29,13 +28,14 @@ public class RenameFeedDialog { } View content = View.inflate(activity, R.layout.edit_text_dialog, null); - EditText editText = content.findViewById(R.id.text); - editText.setText(feed.getTitle()); + EditTextDialogBinding alertViewBinding = EditTextDialogBinding.bind(content); + + alertViewBinding.urlEditText.setText(feed.getTitle()); AlertDialog dialog = new AlertDialog.Builder(activity) .setView(content) .setTitle(de.danoeh.antennapod.core.R.string.rename_feed_label) .setPositiveButton(android.R.string.ok, (d, input) -> { - feed.setCustomTitle(editText.getText().toString()); + feed.setCustomTitle(alertViewBinding.urlEditText.getText().toString()); DBWriter.setFeedCustomTitle(feed); }) .setNeutralButton(de.danoeh.antennapod.core.R.string.reset, null) @@ -44,7 +44,7 @@ public class RenameFeedDialog { // To prevent cancelling the dialog on button click dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener( - (view) -> editText.setText(feed.getFeedTitle())); + (view) -> alertViewBinding.urlEditText.setText(feed.getFeedTitle())); } } 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 274c3b7bd..f1a41d753 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.dialog; import android.app.Activity; import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.view.View; import android.view.inputmethod.InputMethodManager; @@ -88,6 +87,27 @@ public class SleepTimerDialog extends DialogFragment { timeSetup = content.findViewById(R.id.timeSetup); timeDisplay = content.findViewById(R.id.timeDisplay); time = content.findViewById(R.id.time); + Button extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton); + extendSleepFiveMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 5)); + Button extendSleepTenMinutesButton = content.findViewById(R.id.extendSleepTenMinutesButton); + extendSleepTenMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 10)); + Button extendSleepTwentyMinutesButton = content.findViewById(R.id.extendSleepTwentyMinutesButton); + extendSleepTwentyMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 20)); + extendSleepFiveMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(5 * 1000 * 60); + } + }); + extendSleepTenMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(10 * 1000 * 60); + } + }); + extendSleepTwentyMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(20 * 1000 * 60); + } + }); etxtTime.setText(SleepTimerPreferences.lastTimerValue()); etxtTime.postDelayed(() -> { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java index 8a87fef25..29172bb5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java @@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public class SubscriptionsFilterDialog { public static void showDialog(Context context) { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java index 796ec556f..6e894176f 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java @@ -1,7 +1,5 @@ package de.danoeh.antennapod.discovery; -import android.content.Context; -import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java index eeebd2ebf..8fbc8c76b 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.discovery; import io.reactivex.Single; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; import java.util.List; public interface PodcastSearcher { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java index ad574cab6..16c5548be 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java @@ -15,11 +15,11 @@ public class PodcastSearcherRegistry { public static List<SearcherInfo> getSearchProviders() { if (searchProviders == null) { searchProviders = new ArrayList<>(); - searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.f)); + searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f)); searchProviders.add(new SearcherInfo(new GpodnetPodcastSearcher(), 0.0f)); - searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 0.0f)); + searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 1.0f)); } return searchProviders; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index 09dfac37a..ed88214fe 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -12,7 +12,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -30,6 +29,8 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.databinding.AddfeedBinding; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; import de.danoeh.antennapod.discovery.CombinedSearcher; import de.danoeh.antennapod.discovery.FyydPodcastSearcher; import de.danoeh.antennapod.discovery.ItunesPodcastSearcher; @@ -50,7 +51,7 @@ public class AddFeedFragment extends Fragment { private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1; private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2; - private EditText combinedFeedSearchBox; + private AddfeedBinding viewBinding; private MainActivity activity; @Override @@ -59,29 +60,30 @@ public class AddFeedFragment extends Fragment { @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.addfeed, container, false); + viewBinding = AddfeedBinding.inflate(getLayoutInflater()); activity = (MainActivity) getActivity(); - Toolbar toolbar = root.findViewById(R.id.toolbar); + + Toolbar toolbar = viewBinding.toolbar; ((MainActivity) getActivity()).setupToolbarToggle(toolbar); - root.findViewById(R.id.btn_search_itunes).setOnClickListener(v + viewBinding.searchItunesButton.setOnClickListener(v -> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class))); - root.findViewById(R.id.btn_search_fyyd).setOnClickListener(v + viewBinding.searchFyydButton.setOnClickListener(v -> activity.loadChildFragment(OnlineSearchFragment.newInstance(FyydPodcastSearcher.class))); - root.findViewById(R.id.btn_search_gpodder).setOnClickListener(v + viewBinding.searchGPodderButton.setOnClickListener(v -> activity.loadChildFragment(new GpodnetMainFragment())); - root.findViewById(R.id.btn_search_podcastindex).setOnClickListener(v + viewBinding.searchPodcastIndexButton.setOnClickListener(v -> activity.loadChildFragment(OnlineSearchFragment.newInstance(PodcastIndexPodcastSearcher.class))); - combinedFeedSearchBox = root.findViewById(R.id.combinedFeedSearchBox); - combinedFeedSearchBox.setOnEditorActionListener((v, actionId, event) -> { + viewBinding.combinedFeedSearchEditText.setOnEditorActionListener((v, actionId, event) -> { performSearch(); return true; }); - root.findViewById(R.id.btn_add_via_url).setOnClickListener(v + + viewBinding.addViaUrlButton.setOnClickListener(v -> showAddViaUrlDialog()); - root.findViewById(R.id.btn_opml_import).setOnClickListener(v -> { + viewBinding.opmlImportButton.setOnClickListener(v -> { try { Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT); intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE); @@ -93,7 +95,8 @@ public class AddFeedFragment extends Fragment { .showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG); } }); - root.findViewById(R.id.btn_add_local_folder).setOnClickListener(v -> { + + viewBinding.addLocalFolderButton.setOnClickListener(v -> { if (Build.VERSION.SDK_INT < 21) { return; } @@ -108,25 +111,29 @@ public class AddFeedFragment extends Fragment { } }); if (Build.VERSION.SDK_INT < 21) { - root.findViewById(R.id.btn_add_local_folder).setVisibility(View.GONE); + viewBinding.addLocalFolderButton.setVisibility(View.GONE); } - root.findViewById(R.id.search_icon).setOnClickListener(view -> performSearch()); - return root; + + viewBinding.searchButton.setOnClickListener(view -> performSearch()); + + return viewBinding.getRoot(); } private void showAddViaUrlDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(R.string.add_podcast_by_url); View content = View.inflate(getContext(), R.layout.edit_text_dialog, null); - EditText editText = content.findViewById(R.id.text); - editText.setHint(R.string.add_podcast_by_url_hint); + EditTextDialogBinding alertViewBinding = EditTextDialogBinding.bind(content); + alertViewBinding.urlEditText.setHint(R.string.add_podcast_by_url_hint); + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); String clipboardContent = clipboard.getText() != null ? clipboard.getText().toString() : ""; if (clipboardContent.trim().startsWith("http")) { - editText.setText(clipboardContent.trim()); + alertViewBinding.urlEditText.setText(clipboardContent.trim()); } - builder.setView(content); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> addUrl(editText.getText().toString())); + builder.setView(alertViewBinding.getRoot()); + builder.setPositiveButton(R.string.confirm_label, + (dialog, which) -> addUrl(alertViewBinding.urlEditText.getText().toString())); builder.setNegativeButton(R.string.cancel_label, null); builder.show(); } @@ -138,8 +145,7 @@ public class AddFeedFragment extends Fragment { } private void performSearch() { - String query = combinedFeedSearchBox.getText().toString(); - + String query = viewBinding.combinedFeedSearchEditText.getText().toString(); if (query.matches("http[s]?://.*")) { addUrl(query); return; 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 ae3ba3a54..612959c04 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; @@ -105,13 +104,12 @@ public class AllEpisodesFragment extends EpisodesListFragment { @NonNull @Override protected List<FeedItem> loadData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter); } @NonNull @Override protected List<FeedItem> loadMoreData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, - EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter); } } 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 5a65f956c..8ff8866e0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -17,16 +15,21 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; +import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; @@ -43,8 +46,7 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SleepTimerDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.view.PagerIndicatorView; -import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView; +import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -67,14 +69,12 @@ public class AudioPlayerFragment extends Fragment implements private static final int POS_DESCR = 1; private static final int POS_CHAPTERS = 2; private static final int NUM_CONTENT_FRAGMENTS = 3; - private static final String PREFS = "AudioPlayerFragmentPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; + public static final String PREFS = "AudioPlayerFragmentPreferences"; private static final float EPSILON = 0.001f; PlaybackSpeedIndicatorView butPlaybackSpeed; TextView txtvPlaybackSpeed; private ViewPager2 pager; - private PagerIndicatorView pageIndicator; private TextView txtvPosition; private TextView txtvLength; private SeekBar sbPosition; @@ -86,10 +86,14 @@ public class AudioPlayerFragment extends Fragment implements private ImageButton butSkip; private Toolbar toolbar; private ProgressBar progressIndicator; + private CardView cardViewSeek; + private TextView txtvSeek; private PlaybackController controller; private Disposable disposable; private boolean showTimeLeft; + private boolean hasChapters = false; + private TabLayoutMediator tabLayoutMediator; @Override public View onCreateView(@NonNull LayoutInflater inflater, @@ -120,6 +124,8 @@ public class AudioPlayerFragment extends Fragment implements txtvFF = root.findViewById(R.id.txtvFF); butSkip = root.findViewById(R.id.butSkip); progressIndicator = root.findViewById(R.id.progLoading); + cardViewSeek = root.findViewById(R.id.cardViewSeek); + txtvSeek = root.findViewById(R.id.txtvSeek); setupLengthTextView(); setupControlButtons(); @@ -141,13 +147,37 @@ public class AudioPlayerFragment extends Fragment implements }); } }); - pageIndicator = root.findViewById(R.id.page_indicator); - pageIndicator.setViewPager(pager); - pageIndicator.setOnClickListener(v -> - pager.setCurrentItem((pager.getCurrentItem() + 1) % NUM_CONTENT_FRAGMENTS)); + + TabLayout tabLayout = root.findViewById(R.id.sliding_tabs); + tabLayoutMediator = new TabLayoutMediator(tabLayout, pager, (tab, position) -> { + tab.view.setAlpha(1.0f); + switch (position) { + case POS_COVER: + tab.setText(R.string.cover_label); + break; + case POS_DESCR: + tab.setText(R.string.description_label); + break; + case POS_CHAPTERS: + tab.setText(R.string.chapters_label); + if (!hasChapters) { + tab.view.setAlpha(0.5f); + } + break; + default: + break; + } + }); + tabLayoutMediator.attach(); return root; } + public void setHasChapters(boolean hasChapters) { + this.hasChapters = hasChapters; + tabLayoutMediator.detach(); + tabLayoutMediator.attach(); + } + public View getExternalPlayerHolder() { return getView().findViewById(R.id.playerFragment); } @@ -185,16 +215,25 @@ public class AudioPlayerFragment extends Fragment implements IntentUtils.sendLocalBroadcast(getActivity(), PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) { + if (controller == null) { + return; + } + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); + } + private void setupLengthTextView() { - SharedPreferences prefs = getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); txtvLength.setOnClickListener(v -> { if (controller == null) { return; } showTimeLeft = !showTimeLeft; - prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft).apply(); - updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); }); } @@ -362,10 +401,6 @@ public class AudioPlayerFragment extends Fragment implements setupOptionsMenu(media); } - public void setHasChapters(boolean hasChapters) { - pageIndicator.setDisabledPage(hasChapters ? -1 : 2); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -411,6 +446,7 @@ public class AudioPlayerFragment extends Fragment implements return; } txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); if (showTimeLeft) { txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime)); } else { @@ -432,22 +468,22 @@ public class AudioPlayerFragment extends Fragment implements } if (fromUser) { float prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft && prog != 0) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - String length = "-" + Converter.getDurationStringLong(timeLeft); - txtvLength.setText(length); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // interrupt position Observer, restart later + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -456,6 +492,13 @@ public class AudioPlayerFragment extends Fragment implements float prog = seekBar.getProgress() / ((float) seekBar.getMax()); controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } public void setupOptionsMenu(Playable media) { 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 6f95d71da..624345907 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -8,9 +8,9 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; @@ -45,7 +45,8 @@ public class ChaptersFragment extends Fragment { RecyclerView recyclerView = root.findViewById(R.id.recyclerView); layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); - recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation())); adapter = new ChaptersListAdapter(getActivity(), pos -> { if (controller.getStatus() != PlayerStatus.PLAYING) { @@ -106,6 +107,7 @@ public class ChaptersFragment extends Fragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(PlaybackPositionEvent event) { updateChapterSelection(getCurrentChapter(media)); + adapter.notifyTimeChanged(event.getPosition()); } private int getCurrentChapter(Playable media) { 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 59b2cd234..3519a34b4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -4,7 +4,6 @@ import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 648fc614a..60fcac03d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -13,11 +13,9 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; @@ -25,9 +23,11 @@ import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; @@ -35,6 +35,7 @@ import io.reactivex.Maybe; 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; @@ -93,7 +94,12 @@ public class CoverFragment extends Fragment { } private void displayMediaInfo(@NonNull Playable media) { - txtvPodcastTitle.setText(media.getFeedTitle()); + String pubDateStr = DateUtils.formatAbbrev(getActivity(), ((FeedMedia) media).getPubDate()); + txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) + + "\u00A0" + + "・" + + "\u00A0" + + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); txtvEpisodeTitle.setText(media.getEpisodeTitle()); displayedChapterIndex = -2; // Force refresh displayCoverImage(media.getPosition()); @@ -151,23 +157,25 @@ public class CoverFragment extends Fragment { if (chapter != displayedChapterIndex) { displayedChapterIndex = chapter; + RequestOptions options = new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .transforms(new FitCenter(), + new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); + RequestBuilder<Drawable> cover = Glide.with(this) .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))); + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options); + if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) { cover.into(imgvCover); } else { Glide.with(this) .load(EmbeddedChapterImage.getModelFor(media, chapter)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))) + .apply(options) .thumbnail(cover) .error(cover) .into(imgvCover); @@ -208,7 +216,7 @@ public class CoverFragment extends Fragment { imgvCover.setLayoutParams(params); } } else { - double percentageHeight = ratio * 0.8; + double percentageHeight = ratio * 0.6; mainContainer.setOrientation(LinearLayout.HORIZONTAL); if (newConfig.screenHeightDp > 0) { params.height = (int) (convertDpToPixel(newConfig.screenHeightDp) * percentageHeight); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index c2c45c581..2e11ea4ec 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -4,7 +4,6 @@ import android.app.Dialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.res.TypedArray; import android.os.Bundle; import androidx.annotation.NonNull; @@ -13,7 +12,6 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.ListFragment; import android.util.Log; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ListView; 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 8dae310ba..39f935bbe 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -383,6 +383,14 @@ public abstract class EpisodesListFragment extends Fragment { @NonNull protected abstract List<FeedItem> loadData(); + /** + * Load a new page of data as defined by {@link #page} and {@link #EPISODES_PER_PAGE}. + * If the number of items returned is less than {@link #EPISODES_PER_PAGE}, + * it will be assumed that the underlying data is exhausted + * and this method will not be called again. + * + * @return The items from the next page of data + */ @NonNull protected abstract List<FeedItem> loadMoreData(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 5d701472f..f2fe23135 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -198,14 +198,19 @@ public class ExternalPlayerFragment extends Fragment { feedName.setText(media.getFeedTitle()); onPositionObserverUpdate(); + RequestOptions options = new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .fitCenter() + .dontAnimate(); + Glide.with(getActivity()) .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .fitCenter() - .dontAnimate()) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options) .into(imgvCover); if (controller != null && controller.isPlayingVideoLocally()) { 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 abb597e60..dd8a02893 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -45,7 +45,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.StatisticsItem; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.fragment.preferences.StatisticsFragment; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; @@ -130,6 +130,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.visit_website_item) .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.location_web_site)); + toolbar.getMenu().findItem(R.id.share_parent) + .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_share)); } }; iconTintManager.updateTint(); @@ -284,9 +286,13 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic } private void refreshToolbarState() { + boolean shareLinkVisible = feed != null && feed.getLink() != null; + boolean downloadUrlVisible = feed != null && !feed.isLocalFeed(); + toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(feed != null && !feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); + toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(downloadUrlVisible); + toolbar.getMenu().findItem(R.id.share_link_item).setVisible(shareLinkVisible); + toolbar.getMenu().findItem(R.id.share_parent).setVisible(downloadUrlVisible || shareLinkVisible); toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); } 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 583d49afb..0bfb63718 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -57,7 +57,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.Optional; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.FilterDialog; 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 568b56304..fb9ac4a59 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -159,6 +159,7 @@ public class FeedSettingsFragment extends Fragment { setupEpisodeFilterPreference(); setupPlaybackSpeedPreference(); setupFeedAutoSkipPreference(); + setupEpisodeNotificationPreference(); updateAutoDeleteSummary(); updateVolumeReductionValue(); @@ -394,6 +395,19 @@ public class FeedSettingsFragment extends Fragment { } } + private void setupEpisodeNotificationPreference() { + SwitchPreferenceCompat pref = findPreference("episodeNotification"); + + pref.setChecked(feedPreferences.getShowEpisodeNotification()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setShowEpisodeNotification(checked); + feed.savePreferences(); + pref.setChecked(checked); + return false; + }); + } + private class ApplyToEpisodesDialog extends ConfirmationDialog { private final boolean autoDownload; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index fc3052e20..18a61f1e6 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -8,10 +8,8 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; 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 669dbdac2..2fc3d4f0e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -17,7 +17,6 @@ import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.core.text.TextUtilsCompat; import androidx.core.util.ObjectsCompat; import androidx.core.view.ViewCompat; @@ -58,7 +57,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -71,7 +70,6 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -import java.util.Date; import java.util.List; import java.util.Locale; @@ -120,6 +118,7 @@ public class ItemFragment extends Fragment { private View butAction2; private ItemActionButton actionButton1; private ItemActionButton actionButton2; + private View noMediaLabel; private Disposable disposable; private PlaybackController controller; @@ -169,6 +168,7 @@ public class ItemFragment extends Fragment { butAction2Icon = layout.findViewById(R.id.butAction2Icon); butAction1Text = layout.findViewById(R.id.butAction1Text); butAction2Text = layout.findViewById(R.id.butAction2Text); + noMediaLabel = layout.findViewById(R.id.noMediaLabel); butAction1.setOnClickListener(v -> { if (actionButton1 instanceof StreamActionButton && !UserPreferences.isStreamOverDownload() @@ -291,14 +291,19 @@ public class ItemFragment extends Fragment { txtvPublished.setContentDescription(DateUtils.formatForAccessibility(getContext(), item.getPubDate())); } + RequestOptions options = new RequestOptions() + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .transforms(new FitCenter(), + new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) + .dontAnimate(); + Glide.with(getActivity()) .load(ImageResourceUtils.getImageLocation(item)) - .apply(new RequestOptions() - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .transforms(new FitCenter(), - new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) - .dontAnimate()) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(item)) + .apply(options)) + .apply(options) .into(imgvCover); updateButtons(); } @@ -319,7 +324,9 @@ public class ItemFragment extends Fragment { if (media == null) { actionButton1 = new MarkAsPlayedActionButton(item); actionButton2 = new VisitWebsiteActionButton(item); + noMediaLabel.setVisibility(View.VISIBLE); } else { + noMediaLabel.setVisibility(View.GONE); if (media.getDuration() > 0) { txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); txtvDuration.setContentDescription( diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index d748d14c9..1aa66dcbb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; -import android.view.MenuInflater; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.ItemTouchHelper; 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 c994b4d8b..14f355b52 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java @@ -46,6 +46,7 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView. private FeedDiscoverAdapter adapter; private GridView discoverGridLayout; private TextView errorTextView; + private TextView poweredByTextView; private LinearLayout errorView; private Button errorRetry; @@ -63,6 +64,7 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView. errorTextView = root.findViewById(R.id.discover_error_txtV); errorRetry = root.findViewById(R.id.discover_error_retry_btn); errorRetry.setOnClickListener((listener) -> loadToplist()); + poweredByTextView = root.findViewById(R.id.discover_powered_by_itunes); adapter = new FeedDiscoverAdapter((MainActivity) getActivity()); discoverGridLayout.setAdapter(adapter); @@ -110,6 +112,7 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView. discoverGridLayout.setVisibility(View.INVISIBLE); errorView.setVisibility(View.GONE); errorRetry.setVisibility(View.INVISIBLE); + poweredByTextView.setVisibility(View.VISIBLE); ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); SharedPreferences prefs = getActivity().getSharedPreferences(ItunesTopListLoader.PREFS, MODE_PRIVATE); @@ -122,6 +125,7 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView. progressBar.setVisibility(View.GONE); discoverGridLayout.setVisibility(View.INVISIBLE); errorRetry.setVisibility(View.INVISIBLE); + poweredByTextView.setVisibility(View.INVISIBLE); return; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index 087abc327..fc500a223 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import androidx.fragment.app.ListFragment; import android.util.Log; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ListView; 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 0894c2df6..4735cab42 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -27,6 +27,7 @@ import android.widget.TextView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.joanzapata.iconify.Iconify; +import java.util.Locale; import java.util.concurrent.Callable; import de.danoeh.antennapod.R; @@ -65,6 +66,12 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem public static final String TAG = "SubscriptionFragment"; private static final String PREFS = "SubscriptionFragment"; private static final String PREF_NUM_COLUMNS = "columns"; + private static final int MIN_NUM_COLUMNS = 2; + private static final int[] COLUMN_CHECKBOX_IDS = { + R.id.subscription_num_columns_2, + R.id.subscription_num_columns_3, + R.id.subscription_num_columns_4, + R.id.subscription_num_columns_5}; private GridView subscriptionGridLayout; private DBReader.NavDrawerData navDrawerData; @@ -97,6 +104,11 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem toolbar.setOnMenuItemClickListener(this); ((MainActivity) getActivity()).setupToolbarToggle(toolbar); toolbar.inflateMenu(R.menu.subscriptions); + for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) { + // Do this in Java to localize numbers + toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[i]) + .setTitle(String.format(Locale.getDefault(), "%d", i + MIN_NUM_COLUMNS)); + } refreshToolbarState(); subscriptionGridLayout = root.findViewById(R.id.subscriptions_grid); @@ -119,10 +131,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private void refreshToolbarState() { int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); - toolbar.getMenu().findItem(R.id.subscription_num_columns_2).setChecked(columns == 2); - toolbar.getMenu().findItem(R.id.subscription_num_columns_3).setChecked(columns == 3); - toolbar.getMenu().findItem(R.id.subscription_num_columns_4).setChecked(columns == 4); - toolbar.getMenu().findItem(R.id.subscription_num_columns_5).setChecked(columns == 5); + toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(), R.id.refresh_item, updateRefreshMenuItemChecker); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index e272b2869..1f5434688 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -4,12 +4,9 @@ import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; -import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; -import androidx.appcompat.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java index 0d6e79e84..ec61c82f2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java @@ -174,7 +174,9 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { String[] entries = new String[values.length]; for (int x = 0; x < values.length; x++) { int v = Integer.parseInt(values[x]); - if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) { + entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { entries[x] = res.getString(R.string.episode_cleanup_queue_removal); } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ entries[x] = res.getString(R.string.episode_cleanup_never); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java new file mode 100644 index 000000000..187e8480b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java @@ -0,0 +1,302 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Paint; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.ViewFlipper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputLayout; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.IntentUtils; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Guides the user through the authentication process. + */ +public class GpodderAuthenticationFragment extends DialogFragment { + public static final String TAG = "GpodnetAuthActivity"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_HOSTNAME = 0; + private static final int STEP_LOGIN = 1; + private static final int STEP_DEVICE = 2; + private static final int STEP_FINISH = 3; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + private List<GpodnetDevice> devices; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + dialog.setTitle(GpodnetService.DEFAULT_BASE_HOST); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null); + viewFlipper = root.findViewById(R.id.viewflipper); + advance(); + dialog.setView(root); + + return dialog.create(); + } + + private void setupHostView(View view) { + final Button selectHost = view.findViewById(R.id.chooseHostButton); + final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup); + final EditText serverUrlText = view.findViewById(R.id.serverUrlText); + if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHostname())) { + serverUrlText.setText(GpodnetPreferences.getHostname()); + } + final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput); + serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE); + }); + selectHost.setOnClickListener(v -> { + if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) { + GpodnetPreferences.setHostname(serverUrlText.getText().toString()); + } else { + GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); + } + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); + getDialog().setTitle(GpodnetPreferences.getHostname()); + advance(); + }); + } + + private void setupLoginView(View view) { + final EditText username = view.findViewById(R.id.etxtUsername); + final EditText password = view.findViewById(R.id.etxtPassword); + final Button login = view.findViewById(R.id.butLogin); + final TextView txtvError = view.findViewById(R.id.credentialsError); + final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); + final TextView createAccount = view.findViewById(R.id.createAccountButton); + + createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/")); + + password.setOnEditorActionListener((v, actionID, event) -> + actionID == EditorInfo.IME_ACTION_GO && login.performClick()); + + login.setOnClickListener(v -> { + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } + + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + Completable.fromAction(() -> { + service.authenticate(usernameStr, passwordStr); + devices = service.getDevices(); + GpodderAuthenticationFragment.this.username = usernameStr; + GpodderAuthenticationFragment.this.password = passwordStr; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + advance(); + }, error -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getCause().getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + + }); + } + + private void setupDeviceView(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer); + deviceName.setText(generateDeviceName()); + + MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton); + createDeviceButton.setOnClickListener(v -> createDevice(view)); + + for (GpodnetDevice device : devices) { + View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null); + Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton); + selectDeviceButton.setOnClickListener(v -> { + selectedDevice = device; + advance(); + }); + selectDeviceButton.setText(device.getCaption()); + devicesContainer.addView(row); + } + } + + private void createDevice(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final TextView txtvError = view.findViewById(R.id.deviceSelectError); + final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); + + String deviceNameStr = deviceName.getText().toString(); + if (isDeviceInList(deviceNameStr)) { + return; + } + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + deviceName.setEnabled(false); + + Observable.fromCallable(() -> { + String deviceId = generateDeviceId(deviceNameStr); + service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(device -> { + progBarCreateDevice.setVisibility(View.GONE); + selectedDevice = device; + advance(); + }, error -> { + deviceName.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + } + + private String generateDeviceName() { + String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL); + String name = baseName; + int num = 1; + while (isDeviceInList(name)) { + name = baseName + " (" + num + ")"; + num++; + } + return name; + } + + private String generateDeviceId(String name) { + // devices names must be of a certain form: + // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices + return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US); + } + + private boolean isDeviceInList(String name) { + if (devices == null) { + return false; + } + String id = generateDeviceId(name); + for (GpodnetDevice device : devices) { + if (device.getId().equals(id) || device.getCaption().equals(name)) { + return true; + } + } + return false; + } + + private GpodnetDevice findDevice(String id) { + if (devices == null) { + return null; + } + for (GpodnetDevice device : devices) { + if (device.getId().equals(id)) { + return device; + } + } + return null; + } + + private void setupFinishView(View view) { + final Button sync = view.findViewById(R.id.butSyncNow); + + sync.setOnClickListener(v -> { + dismiss(); + SyncService.sync(getContext()); + }); + } + + private void writeLoginCredentials() { + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = viewFlipper.getChildAt(currentStep + 1); + if (currentStep == STEP_DEFAULT) { + setupHostView(view); + } else if (currentStep == STEP_HOSTNAME) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + dismiss(); + } + } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java index bec73894c..4fb734e17 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java @@ -1,15 +1,12 @@ package de.danoeh.antennapod.fragment.preferences; import android.app.Activity; -import android.content.SharedPreferences; import android.os.Bundle; import androidx.core.text.HtmlCompat; -import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import android.text.Spanned; import android.text.format.DateUtils; -import android.widget.Toast; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; @@ -17,19 +14,16 @@ import de.danoeh.antennapod.core.event.SyncServiceEvent; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; - public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; - private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -54,6 +48,7 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void syncStatusChanged(SyncServiceEvent event) { + updateGpodnetPreferenceScreen(); if (!GpodnetPreferences.loggedIn()) { return; } @@ -69,6 +64,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private void setupGpodderScreen() { final Activity activity = getActivity(); + findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> { + new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG); + return true; + }); findPreference(PREF_GPODNET_SETLOGIN_INFORMATION) .setOnPreferenceClickListener(preference -> { AuthenticationDialog dialog = new AuthenticationDialog(activity, @@ -97,11 +96,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { updateGpodnetPreferenceScreen(); return true; }); - findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(preference -> { - GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener( - dialog -> updateGpodnetPreferenceScreen()); - return true; - }); } private void updateGpodnetPreferenceScreen() { @@ -122,7 +116,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { } else { findPreference(PREF_GPODNET_LOGOUT).setSummary(null); } - findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); } private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { 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 99fd12021..7bf602e35 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 @@ -1,8 +1,11 @@ package de.danoeh.antennapod.fragment.preferences; import android.content.Intent; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.bytehamster.lib.preferencesearch.SearchConfiguration; import com.bytehamster.lib.preferencesearch.SearchPreference; @@ -20,7 +23,7 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork"; private static final String PREF_SCREEN_GPODDER = "prefScreenGpodder"; private static final String PREF_SCREEN_STORAGE = "prefScreenStorage"; - private static final String PREF_FAQ = "prefFaq"; + private static final String PREF_DOCUMENTATION = "prefDocumentation"; private static final String PREF_VIEW_FORUM = "prefViewForum"; private static final String PREF_SEND_BUG_REPORT = "prefSendBugReport"; private static final String PREF_CATEGORY_PROJECT = "project"; @@ -35,10 +38,18 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { setupSearch(); // If you are writing a spin-off, please update the details on screens like "About" and "Report bug" - // and afterwards remove the following lines. + // and afterwards remove the following lines. Please keep in mind that AntennaPod is licensed under the GPL. + // This means that your application needs to be open-source under the GPL, too. + // It must also include a prominent copyright notice. String packageName = getContext().getPackageName(); if (!"de.danoeh.antennapod".equals(packageName) && !"de.danoeh.antennapod.debug".equals(packageName)) { findPreference(PREF_CATEGORY_PROJECT).setVisible(false); + Preference copyrightNotice = new Preference(getContext()); + copyrightNotice.setSummary("This application is based on AntennaPod." + + " The AntennaPod team does NOT provide support for this unofficial version." + + " If you can read this message, the developers of this modification" + + " violate the GNU General Public License (GPL)."); + findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(copyrightNotice); } } @@ -70,10 +81,16 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { return true; }); findPreference(PREF_NOTIFICATION).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); + if (Build.VERSION.SDK_INT >= 26) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName()); + startActivity(intent); + } else { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); + } return true; }); - findPreference(PREF_ABOUT).setOnPreferenceClickListener( preference -> { getParentFragmentManager().beginTransaction().replace(R.id.content, new AboutFragment()) @@ -88,8 +105,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { return true; } ); - findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> { - IntentUtils.openInBrowser(getContext(), "https://antennapod.org/faq.html"); + findPreference(PREF_DOCUMENTATION).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), "https://antennapod.org/documentation/"); return true; }); findPreference(PREF_VIEW_FORUM).setOnPreferenceClickListener(preference -> { 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 index fcc37f644..3889034fa 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java @@ -15,7 +15,6 @@ import org.apache.commons.lang3.ArrayUtils; import java.util.Calendar; import java.util.GregorianCalendar; -import java.util.Locale; import java.util.concurrent.TimeUnit; public class NetworkPreferencesFragment extends PreferenceFragmentCompat { @@ -72,13 +71,13 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { Context context = getActivity().getApplicationContext(); String val; long interval = UserPreferences.getUpdateInterval(); - if(interval > 0) { + if (interval > 0) { int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); - String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr); + val = context.getResources().getQuantityString( + R.plurals.pref_autoUpdateIntervallOrTime_every_hours, hours, hours); } else { int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - if(timeOfDay.length == 2) { + if (timeOfDay.length == 2) { Calendar cal = new GregorianCalendar(); cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); cal.set(Calendar.MINUTE, timeOfDay[1]); @@ -97,8 +96,7 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { private void setParallelDownloadsText(int downloads) { final Resources res = getActivity().getResources(); - String s = String.format(Locale.getDefault(), "%d%s", - downloads, res.getString(R.string.parallel_downloads_suffix)); + String s = res.getString(R.string.parallel_downloads, downloads); findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); } 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 689a72ba7..4d1b79965 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 @@ -9,11 +9,14 @@ import androidx.preference.PreferenceFragmentCompat; import android.widget.ListView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.event.PlayerStatusEvent; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.fragment.NavDrawerFragment; import org.apache.commons.lang3.ArrayUtils; +import org.greenrobot.eventbus.EventBus; import java.util.List; @@ -37,8 +40,17 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { (preference, newValue) -> { getActivity().recreate(); return true; - } - ); + }); + + findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + UserPreferences.setShowRemainTimeSetting((Boolean) newValue); + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + EventBus.getDefault().post(new PlayerStatusEvent()); + return true; + }); + findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) .setOnPreferenceClickListener(preference -> { showDrawerPreferencesDialog(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java index b440d053b..0a64bbe71 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java @@ -33,7 +33,7 @@ public class AboutFragment extends PreferenceFragmentCompat { return true; }); findPreference("about_privacy_policy").setOnPreferenceClickListener((preference) -> { - IntentUtils.openInBrowser(getContext(), "https://antennapod.org/privacy.html"); + IntentUtils.openInBrowser(getContext(), "https://antennapod.org/privacy/"); return true; }); findPreference("about_licenses").setOnPreferenceClickListener((preference) -> { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java index 60d9f95dd..b844234b7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java @@ -6,8 +6,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.adapter.SimpleIconListAdapter; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java index 6db1389ea..d759a5ff2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java @@ -6,8 +6,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.adapter.SimpleIconListAdapter; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java index e8d8e113b..b77c74de6 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java @@ -6,8 +6,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.adapter.SimpleIconListAdapter; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; 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 9ceed9369..0086a75ab 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.DialogInterface; import android.util.Log; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import androidx.annotation.NonNull; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index 9c54a529b..fbfdf537f 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -9,7 +9,7 @@ import androidx.appcompat.widget.SearchView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.fragment.SearchFragment; import java.util.HashMap; 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 311f44881..03a8edbf0 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.preferences; import android.content.Context; import android.content.SharedPreferences; +import android.view.KeyEvent; import androidx.preference.PreferenceManager; import de.danoeh.antennapod.BuildConfig; @@ -92,5 +93,16 @@ public class PreferenceUpgrader { if (oldVersion < 1080100) { prefs.edit().putString(UserPreferences.PREF_VIDEO_BEHAVIOR, "pip").apply(); } + if (oldVersion < 2010300) { + // Migrate hardware button preferences + if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply(); + } + if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); + } + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java b/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java deleted file mode 100644 index f755a4c84..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java +++ /dev/null @@ -1,92 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.ThemeUtils; - -public class CircularProgressBar extends View { - private static final float EPSILON = 0.005f; - - private final Paint paintBackground = new Paint(); - private final Paint paintProgress = new Paint(); - private float percentage = 0; - private float targetPercentage = 0; - private Object tag = null; - private final RectF bounds = new RectF(); - - public CircularProgressBar(Context context) { - super(context); - setup(); - } - - public CircularProgressBar(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - private void setup() { - paintBackground.setAntiAlias(true); - paintBackground.setStyle(Paint.Style.STROKE); - - paintProgress.setAntiAlias(true); - paintProgress.setStyle(Paint.Style.STROKE); - paintProgress.setStrokeCap(Paint.Cap.ROUND); - - int color = ThemeUtils.getColorFromAttr(getContext(), R.attr.action_icon_color); - paintProgress.setColor(color); - paintBackground.setColor(color); - } - - /** - * Sets the percentage to be displayed. - * @param percentage Number from 0 to 1 - * @param tag When the tag is the same as last time calling setPercentage, the update is animated - */ - public void setPercentage(float percentage, Object tag) { - targetPercentage = percentage; - - if (tag == null || !tag.equals(this.tag)) { - // Do not animate - this.percentage = percentage; - this.tag = tag; - } - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - float padding = getHeight() * 0.07f; - paintBackground.setStrokeWidth(getHeight() * 0.02f); - paintProgress.setStrokeWidth(padding); - bounds.set(padding, padding, getWidth() - padding, getHeight() - padding); - canvas.drawArc(bounds, 0, 360, false, paintBackground); - - if (percentage > EPSILON && 1 - percentage > EPSILON) { - canvas.drawArc(bounds, -90, percentage * 360, false, paintProgress); - } - - if (Math.abs(percentage - targetPercentage) > EPSILON) { - float speed = 0.02f; - if (Math.abs(targetPercentage - percentage) < 0.1 && targetPercentage > percentage) { - speed = 0.006f; - } - float delta = Math.min(speed, Math.abs(targetPercentage - percentage)); - float direction = ((targetPercentage - percentage) > 0 ? 1f : -1f); - percentage += delta * direction; - invalidate(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java index 83d90f98b..fb1c533c5 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java @@ -6,9 +6,9 @@ import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import androidx.appcompat.view.ContextThemeWrapper; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import io.reactivex.annotations.Nullable; @@ -39,7 +39,7 @@ public class EpisodeItemListRecyclerView extends RecyclerView { layoutManager.setRecycleChildrenOnDetach(true); setLayoutManager(layoutManager); setHasFixedSize(true); - addItemDecoration(new HorizontalDividerItemDecoration.Builder(getContext()).build()); + addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation())); setClipToPadding(false); } diff --git a/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java b/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java deleted file mode 100644 index 10ed98769..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java +++ /dev/null @@ -1,124 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.animation.ArgbEvaluator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; -import androidx.core.text.TextUtilsCompat; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.widget.ViewPager2; - -import java.util.Locale; - -public class PagerIndicatorView extends View { - private final Paint paint = new Paint(); - private float position = 0; - private int numPages = 0; - private int disabledPage = -1; - private int circleColor = 0; - private int circleColorHighlight = -1; - private boolean isLocaleRtl = false; - - public PagerIndicatorView(Context context) { - super(context); - setup(); - } - - public PagerIndicatorView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public PagerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - private void setup() { - isLocaleRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) - == ViewCompat.LAYOUT_DIRECTION_RTL; - - paint.setAntiAlias(true); - paint.setStyle(Paint.Style.FILL); - - int[] colorAttrs = new int[]{android.R.attr.textColorSecondary}; - TypedArray a = getContext().obtainStyledAttributes(colorAttrs); - circleColorHighlight = a.getColor(0, 0xffffffff); - circleColor = (Integer) new ArgbEvaluator().evaluate(0.8f, 0x00ffffff, circleColorHighlight); - a.recycle(); - } - - /** - * Visual and logical position distinction only happens in RTL locales (e.g. Persian) - * where pages positions are flipped thus it does nothing in LTR locales (e.g. English) - */ - private float logicalPositionToVisual(float position) { - return isLocaleRtl ? numPages - 1 - position : position; - } - - public void setViewPager(ViewPager2 pager) { - numPages = pager.getAdapter().getItemCount(); - pager.getAdapter().registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - numPages = pager.getAdapter().getItemCount(); - invalidate(); - } - }); - pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - PagerIndicatorView.this.position = logicalPositionToVisual( - position + positionOffset); - invalidate(); - } - }); - } - - public void setDisabledPage(int disabledPage) { - this.disabledPage = (int) logicalPositionToVisual(disabledPage); - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - for (int i = 0; i < numPages; i++) { - if ((int) Math.floor(position) == i) { - // This is the current dot - drawCircle(canvas, i, (float) (1 - (position - Math.floor(position)))); - } else if ((int) Math.ceil(position) == i) { - // This is the next dot - drawCircle(canvas, i, (float) (position - Math.floor(position))); - } else { - drawCircle(canvas, i, 0); - } - } - } - - private void drawCircle(Canvas canvas, int position, float frac) { - float availableHeight = canvas.getHeight() - getPaddingTop() - getPaddingBottom(); - float circleRadiusSmall = availableHeight * 0.26f; - float circleRadiusBig = availableHeight * 0.35f; - float circleRadiusDelta = (circleRadiusBig - circleRadiusSmall); - float start = 0.5f * (canvas.getWidth() - numPages * 1.5f * availableHeight); - paint.setStrokeWidth(availableHeight * 0.3f); - - if (position == disabledPage) { - paint.setStyle(Paint.Style.STROKE); - } else { - paint.setStyle(Paint.Style.FILL_AND_STROKE); - } - - paint.setColor((Integer) new ArgbEvaluator().evaluate(frac, circleColor, circleColorHighlight)); - canvas.drawCircle(start + (position * 1.5f + 0.75f) * availableHeight, 0.5f * availableHeight + getPaddingTop(), - circleRadiusSmall + frac * circleRadiusDelta, paint); - } -}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedIndicatorView.java b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedIndicatorView.java deleted file mode 100644 index d7f1eac1d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedIndicatorView.java +++ /dev/null @@ -1,113 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.R; - -public class PlaybackSpeedIndicatorView extends View { - private static final float DEG_2_RAD = (float) (Math.PI / 180); - private static final float PADDING_ANGLE = 30; - private static final float VALUE_UNSET = -4242; - - private final Paint arcPaint = new Paint(); - private final Paint indicatorPaint = new Paint(); - private final Path trianglePath = new Path(); - private float angle = VALUE_UNSET; - private float targetAngle = VALUE_UNSET; - private float degreePerFrame = 2; - private float paddingArc = 20; - private float paddingIndicator = 10; - private RectF arcBounds = new RectF(); - - public PlaybackSpeedIndicatorView(Context context) { - super(context); - setup(); - } - - public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - private void setup() { - setSpeed(1.0f); // Set default angle to 1.0 - targetAngle = VALUE_UNSET; // Do not move to first value that is set externally - - int[] colorAttrs = new int[] {R.attr.action_icon_color }; - TypedArray a = getContext().obtainStyledAttributes(colorAttrs); - arcPaint.setColor(a.getColor(0, 0xffffffff)); - indicatorPaint.setColor(a.getColor(0, 0xffffffff)); - a.recycle(); - - arcPaint.setAntiAlias(true); - arcPaint.setStyle(Paint.Style.STROKE); - arcPaint.setStrokeCap(Paint.Cap.ROUND); - - indicatorPaint.setAntiAlias(true); - indicatorPaint.setStyle(Paint.Style.FILL); - - trianglePath.setFillType(Path.FillType.EVEN_ODD); - } - - public void setSpeed(float value) { - float maxAnglePerDirection = 90 + 45 - 2 * paddingArc; - // Speed values above 3 are probably not too common. Cap at 3 for better differentiation - float normalizedValue = Math.min(2.5f, value - 0.5f) / 2.5f; // Linear between 0 and 1 - float target = -maxAnglePerDirection + 2 * maxAnglePerDirection * normalizedValue; - if (targetAngle == VALUE_UNSET) { - angle = target; - } - targetAngle = target; - degreePerFrame = Math.abs(targetAngle - angle) / 20; - invalidate(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - paddingArc = getMeasuredHeight() / 4.5f; - paddingIndicator = getMeasuredHeight() / 6f; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - float radiusInnerCircle = getWidth() / 10f; - canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, radiusInnerCircle, indicatorPaint); - - trianglePath.rewind(); - float bigRadius = getHeight() / 2f - paddingIndicator; - trianglePath.moveTo(getWidth() / 2f + (float) (bigRadius * Math.sin((-angle + 180) * DEG_2_RAD)), - getHeight() / 2f + (float) (bigRadius * Math.cos((-angle + 180) * DEG_2_RAD))); - trianglePath.lineTo(getWidth() / 2f + (float) (radiusInnerCircle * Math.sin((-angle + 180 - 90) * DEG_2_RAD)), - getHeight() / 2f + (float) (radiusInnerCircle * Math.cos((-angle + 180 - 90) * DEG_2_RAD))); - trianglePath.lineTo(getWidth() / 2f + (float) (radiusInnerCircle * Math.sin((-angle + 180 + 90) * DEG_2_RAD)), - getHeight() / 2f + (float) (radiusInnerCircle * Math.cos((-angle + 180 + 90) * DEG_2_RAD))); - trianglePath.close(); - canvas.drawPath(trianglePath, indicatorPaint); - - arcPaint.setStrokeWidth(getHeight() / 15f); - arcBounds.set(paddingArc, paddingArc, getWidth() - paddingArc, getHeight() - paddingArc); - canvas.drawArc(arcBounds, -180 - 45, 90 + 45 + angle - PADDING_ANGLE, false, arcPaint); - canvas.drawArc(arcBounds, -90 + PADDING_ANGLE + angle, 90 + 45 - PADDING_ANGLE - angle, false, arcPaint); - - if (Math.abs(angle - targetAngle) > 0.5 && targetAngle != VALUE_UNSET) { - angle += Math.signum(targetAngle - angle) * Math.min(degreePerFrame, Math.abs(targetAngle - angle)); - invalidate(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/RecursiveRadioGroup.java b/app/src/main/java/de/danoeh/antennapod/view/RecursiveRadioGroup.java deleted file mode 100644 index ee5e7c51d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/RecursiveRadioGroup.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import java.util.ArrayList; - -/** - * An alternative to {@link android.widget.RadioGroup} that allows to nest children. - * Basend on https://stackoverflow.com/a/14309274. - */ -public class RecursiveRadioGroup extends LinearLayout { - private final ArrayList<RadioButton> radioButtons = new ArrayList<>(); - private RadioButton checkedButton = null; - - public RecursiveRadioGroup(Context context) { - super(context); - } - - public RecursiveRadioGroup(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public RecursiveRadioGroup(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - super.addView(child, index, params); - parseChild(child); - } - - public void parseChild(final View child) { - if (child instanceof RadioButton) { - RadioButton button = (RadioButton) child; - radioButtons.add(button); - button.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (!isChecked) { - return; - } - checkedButton = (RadioButton) buttonView; - - for (RadioButton view : radioButtons) { - if (view != buttonView) { - view.setChecked(false); - } - } - }); - } else if (child instanceof ViewGroup) { - parseChildren((ViewGroup) child); - } - } - - public void parseChildren(final ViewGroup child) { - for (int i = 0; i < child.getChildCount(); i++) { - parseChild(child.getChildAt(i)); - } - } - - public RadioButton getCheckedButton() { - return checkedButton; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java b/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java deleted file mode 100644 index c256ede9e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.content.res.TypedArray; -import androidx.appcompat.widget.AppCompatImageView; -import android.util.AttributeSet; -import de.danoeh.antennapod.core.R; - -/** - * From http://stackoverflow.com/a/19449488/6839 - */ -public class SquareImageView extends AppCompatImageView { - public static final int DIRECTION_WIDTH = 0; - public static final int DIRECTION_HEIGHT = 1; - public static final int DIRECTION_MINIMUM = 2; - - private int direction = DIRECTION_WIDTH; - - public SquareImageView(Context context) { - super(context); - } - - public SquareImageView(Context context, AttributeSet attrs) { - super(context, attrs); - loadAttrs(context, attrs); - } - - public SquareImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - loadAttrs(context, attrs); - } - - private void loadAttrs(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SquareImageView); - direction = a.getInt(R.styleable.SquareImageView_direction, DIRECTION_WIDTH); - a.recycle(); - } - - public void setDirection(int direction) { - this.direction = direction; - requestLayout(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - switch (direction) { - case DIRECTION_MINIMUM: - int size = Math.min(getMeasuredWidth(), getMeasuredHeight()); - setMeasuredDimension(size, size); - break; - case DIRECTION_HEIGHT: - setMeasuredDimension(getMeasuredHeight(), getMeasuredHeight()); - break; - default: - setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); - break; - } - } - -}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java b/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java deleted file mode 100644 index 37792b4d1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.GridView; - -/** - * Source: https://stackoverflow.com/a/46350213/ - */ -public class WrappingGridView extends GridView { - - public WrappingGridView(Context context) { - super(context); - } - - public WrappingGridView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public WrappingGridView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int heightSpec = heightMeasureSpec; - if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) { - // The great Android "hackatlon", the love, the magic. - // The two leftmost bits in the height measure spec have - // a special meaning, hence we can't use them to describe height. - heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); - } - super.onMeasure(widthMeasureSpec, heightSpec); - } -} 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 35744227f..0e84afe04 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 @@ -13,9 +13,7 @@ import android.widget.TextView; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; - import com.joanzapata.iconify.Iconify; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; @@ -25,13 +23,15 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; /** * Holds the view which shows FeedItems. @@ -132,9 +132,6 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { private void bind(FeedMedia media) { isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); - duration.setText(Converter.getDurationStringLong(media.getDuration())); - duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, media.getDuration()))); if (media.isCurrentlyPlaying()) { itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background)); @@ -152,6 +149,9 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% } + duration.setText(Converter.getDurationStringLong(media.getDuration())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, media.getDuration()))); if (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS) { int progress = (int) (100.0 * media.getPosition() / media.getDuration()); progressBar.setProgress(progress); @@ -160,6 +160,11 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { Converter.getDurationStringLocalized(activity, media.getPosition()))); progressBar.setVisibility(View.VISIBLE); position.setVisibility(View.VISIBLE); + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition())))); + } } else { progressBar.setVisibility(View.GONE); position.setVisibility(View.GONE); @@ -186,6 +191,22 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } } + private void updateDuration(PlaybackPositionEvent event) { + int currentPosition = event.getPosition(); + int timeDuration = event.getDuration(); + int remainingTime = event.getDuration() - event.getPosition(); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == PlaybackService.INVALID_TIME || timeDuration == PlaybackService.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(remainingTime)); + } else { + duration.setText(Converter.getDurationStringLong(timeDuration)); + } + } + public FeedItem getFeedItem() { return item; } @@ -197,7 +218,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); position.setText(Converter.getDurationStringLong(event.getPosition())); - duration.setText(Converter.getDurationStringLong(event.getDuration())); + updateDuration(event); duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known } diff --git a/app/src/main/res/layout/activity_widget_config.xml b/app/src/main/res/layout/activity_widget_config.xml index ca8aba52d..6e31aec0d 100644 --- a/app/src/main/res/layout/activity_widget_config.xml +++ b/app/src/main/res/layout/activity_widget_config.xml @@ -22,7 +22,7 @@ android:id="@+id/widget_config_preview" layout="@layout/player_widget" android:layout_width="match_parent" - android:layout_height="80dp" + android:layout_height="96dp" android:layout_gravity="center" android:layout_margin="16dp" /> </FrameLayout> @@ -68,13 +68,38 @@ android:max="100" android:progress="100" /> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <CheckBox + android:id="@+id/ckRewind" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Rewind" /> + + <CheckBox + android:id="@+id/ckFastForward" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Forward" /> + + <CheckBox + android:id="@+id/ckSkip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Skip" /> + </LinearLayout> <Button android:id="@+id/butConfirm" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/widget_create_button" /> - </LinearLayout> </LinearLayout> diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index ee57fdbed..d6ebd58d7 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -35,11 +35,11 @@ android:layout_marginRight="8dp" android:contentDescription="@string/search_podcast_hint" app:srcCompat="?attr/action_search" - android:id="@+id/search_icon" + android:id="@+id/searchButton" android:scaleType="center"/> <EditText - android:id="@+id/combinedFeedSearchBox" + android:id="@+id/combinedFeedSearchEditText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" @@ -87,7 +87,7 @@ android:textColor="?android:attr/textColorPrimary"/> <TextView - android:id="@+id/btn_add_via_url" + android:id="@+id/addViaUrlButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/feed" @@ -96,7 +96,7 @@ android:text="@string/add_podcast_by_url"/> <TextView - android:id="@+id/btn_add_local_folder" + android:id="@+id/addLocalFolderButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/ic_folder" @@ -105,7 +105,7 @@ android:text="@string/add_local_folder"/> <TextView - android:id="@+id/btn_search_itunes" + android:id="@+id/searchItunesButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/action_search" @@ -114,7 +114,7 @@ android:text="@string/search_itunes_label"/> <TextView - android:id="@+id/btn_search_fyyd" + android:id="@+id/searchFyydButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/action_search" @@ -123,7 +123,7 @@ android:text="@string/search_fyyd_label"/> <TextView - android:id="@+id/btn_search_gpodder" + android:id="@+id/searchGPodderButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/action_search" @@ -132,7 +132,7 @@ android:text="@string/browse_gpoddernet_label"/> <TextView - android:id="@+id/btn_search_podcastindex" + android:id="@+id/searchPodcastIndexButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/action_search" @@ -141,7 +141,7 @@ android:text="@string/search_podcastindex_label"/> <TextView - android:id="@+id/btn_opml_import" + android:id="@+id/opmlImportButton" android:layout_width="match_parent" android:layout_height="wrap_content" app:drawableStartCompat="?attr/av_download" diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index 225fdd98c..62e0a5e3a 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -15,15 +15,15 @@ app:navigationIcon="?homeAsUpIndicator" android:id="@+id/toolbar"/> - <de.danoeh.antennapod.view.PagerIndicatorView - android:id="@+id/page_indicator" - android:layout_height="16dp" - android:layout_width="40dp" - android:layout_marginTop="-12dp" - android:padding="4dp" + <com.google.android.material.tabs.TabLayout + android:id="@+id/sliding_tabs" + android:layout_width="match_parent" + android:layout_height="wrap_content" android:layout_below="@id/toolbar" - android:contentDescription="@string/switch_pages" - android:layout_centerHorizontal="true"/> + android:background="?android:attr/windowBackground" + app:tabBackground="?attr/selectableItemBackground" + app:tabMode="fixed" + app:tabGravity="fill"/> <FrameLayout android:id="@+id/playerFragment" @@ -39,7 +39,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_above="@id/playtime_layout" - android:layout_below="@id/toolbar" + android:layout_below="@id/sliding_tabs" android:foreground="?android:windowContentOverlay" android:layout_marginBottom="12dp"/> @@ -51,6 +51,34 @@ app:tint="?android:attr/windowBackground" android:importantForAccessibility="no"/> + <androidx.cardview.widget.CardView + android:id="@+id/cardViewSeek" + android:alpha="0" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/pager" + android:layout_centerHorizontal="true" + android:layout_marginBottom="12dp" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?attr/seek_background" + app:cardElevation="0dp" + tools:alpha="1"> + + <TextView + android:id="@+id/txtvSeek" + android:gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="24dp" + android:paddingTop="4dp" + android:paddingRight="24dp" + android:paddingBottom="4dp" + android:textColor="@color/white" + android:textSize="24sp" + tools:text="1:06:29" /> + + </androidx.cardview.widget.CardView> + <LinearLayout android:id="@+id/playtime_layout" android:layout_width="match_parent" @@ -128,7 +156,7 @@ android:scaleType="fitCenter" tools:srcCompat="@drawable/ic_av_play_white_24dp"/> - <de.danoeh.antennapod.view.CircularProgressBar + <de.danoeh.antennapod.ui.common.CircularProgressBar android:layout_width="@dimen/audioplayer_playercontrols_length_big" android:layout_height="@dimen/audioplayer_playercontrols_length_big" android:layout_marginLeft="16dp" @@ -136,7 +164,8 @@ android:layout_marginRight="16dp" android:layout_marginEnd="16dp" android:layout_centerHorizontal="true" - android:layout_centerVertical="true"/> + android:layout_centerVertical="true" + app:foregroundColor="?attr/action_icon_color"/> <ProgressBar style="?android:attr/progressBarStyle" @@ -177,7 +206,7 @@ android:textColor="?android:attr/textColorSecondary" android:clickable="false"/> - <de.danoeh.antennapod.view.PlaybackSpeedIndicatorView + <de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView android:id="@+id/butPlaybackSpeed" android:layout_width="@dimen/audioplayer_playercontrols_length" android:layout_height="@dimen/audioplayer_playercontrols_length" @@ -186,7 +215,8 @@ android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/playback_speed" - tools:srcCompat="@drawable/ic_playback_speed_white"/> + tools:srcCompat="@drawable/ic_playback_speed_white" + app:foregroundColor="?attr/action_icon_color"/> <TextView android:id="@+id/txtvPlaybackSpeed" diff --git a/app/src/main/res/layout/authentication_dialog.xml b/app/src/main/res/layout/authentication_dialog.xml index 9c6f3e2bb..f311fc1dd 100644 --- a/app/src/main/res/layout/authentication_dialog.xml +++ b/app/src/main/res/layout/authentication_dialog.xml @@ -1,30 +1,59 @@ <?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" > - - <EditText - android:id="@+id/etxtUsername" - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:layout_margin="16dp" - android:hint="@string/username_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <EditText - android:id="@+id/etxtPassword" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:layout_margin="16dp" - android:inputType="textPassword" - android:hint="@string/password_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/usernameEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:lines="1"/> + + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/passwordEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:inputType="textPassword" + android:lines="1"/> + + </com.google.android.material.textfield.TextInputLayout> + + <com.joanzapata.iconify.widget.IconTextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/showPasswordButton" + android:text="{fa-eye}" + android:padding="8dp" + android:textColor="?android:attr/textColorPrimary" + android:background="?attr/selectableItemBackgroundBorderless" + android:alpha="0.6" + android:textSize="20sp" + android:layout_marginLeft="8dp" + android:layout_marginStart="8dp"/> + </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/bug_report.xml b/app/src/main/res/layout/bug_report.xml index 1cc9bc9d8..e97e85265 100644 --- a/app/src/main/res/layout/bug_report.xml +++ b/app/src/main/res/layout/bug_report.xml @@ -16,12 +16,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content"/> - <Button - android:id="@+id/btn_export_logcat" - android:text="@string/export_logs" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> - <TextView android:layout_marginTop="8dp" android:id="@+id/crash_report_logs" diff --git a/app/src/main/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml index 5460d0609..0ec46cbcd 100644 --- a/app/src/main/res/layout/cover_fragment.xml +++ b/app/src/main/res/layout/cover_fragment.xml @@ -10,7 +10,7 @@ android:padding="8dp" android:gravity="center"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/imgvCover" android:layout_width="0dp" android:layout_height="200dp" diff --git a/app/src/main/res/layout/download_authentication_activity.xml b/app/src/main/res/layout/download_authentication_activity.xml deleted file mode 100644 index e16a8b3a8..000000000 --- a/app/src/main/res/layout/download_authentication_activity.xml +++ /dev/null @@ -1,64 +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" - android:padding="16dp"> - - <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/authentication_notification_title" - android:textSize="@dimen/text_size_large" - android:textColor="?android:attr/textColorPrimary"/> - - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/authentication_notification_msg" - android:textColor="?android:attr/textColorSecondary"/> - - <EditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/etxtUsername" - android:hint="@string/username_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <EditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/etxtPassword" - android:hint="@string/password_label" - android:inputType="textPassword" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="48dp" - android:orientation="horizontal" - android:gravity="end"> - - <Button - android:id="@+id/butCancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/cancel_label" - style="@style/Widget.MaterialComponents.Button.TextButton"/> - - <Button - android:id="@+id/butConfirm" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/confirm_label" - style="@style/Widget.MaterialComponents.Button.TextButton"/> - </LinearLayout> - -</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/edit_text_dialog.xml b/app/src/main/res/layout/edit_text_dialog.xml index 6bf0bc6cb..b442b92ce 100644 --- a/app/src/main/res/layout/edit_text_dialog.xml +++ b/app/src/main/res/layout/edit_text_dialog.xml @@ -10,6 +10,6 @@ android:layout_height="wrap_content" android:inputType="text" android:ems="10" - android:id="@+id/text" /> + android:id="@+id/urlEditText" /> </LinearLayout> diff --git a/app/src/main/res/layout/feeditem_fragment.xml b/app/src/main/res/layout/feeditem_fragment.xml index 72effc585..049182803 100644 --- a/app/src/main/res/layout/feeditem_fragment.xml +++ b/app/src/main/res/layout/feeditem_fragment.xml @@ -166,6 +166,15 @@ </LinearLayout> </LinearLayout> + <TextView + android:id="@+id/noMediaLabel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone" + android:textAlignment="center" + android:background="?android:attr/dividerVertical" + android:text="@string/no_media_label"/> + <View android:layout_width="match_parent" android:layout_height="1dp" diff --git a/app/src/main/res/layout/filter_dialog_row.xml b/app/src/main/res/layout/filter_dialog_row.xml index 5011812d9..914525387 100644 --- a/app/src/main/res/layout/filter_dialog_row.xml +++ b/app/src/main/res/layout/filter_dialog_row.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<de.danoeh.antennapod.view.RecursiveRadioGroup +<de.danoeh.antennapod.ui.common.RecursiveRadioGroup xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -58,4 +58,4 @@ android:layout_gravity="center_vertical" android:checked="true" /> -</de.danoeh.antennapod.view.RecursiveRadioGroup> +</de.danoeh.antennapod.ui.common.RecursiveRadioGroup> diff --git a/app/src/main/res/layout/gpodnetauth_activity.xml b/app/src/main/res/layout/gpodnetauth_activity.xml deleted file mode 100644 index c096c20cf..000000000 --- a/app/src/main/res/layout/gpodnetauth_activity.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> -<ViewFlipper - android:id="@+id/viewflipper" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> -</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_credentials.xml b/app/src/main/res/layout/gpodnetauth_credentials.xml index 895b0999c..291b98da3 100644 --- a/app/src/main/res/layout/gpodnetauth_credentials.xml +++ b/app/src/main/res/layout/gpodnetauth_credentials.xml @@ -1,96 +1,97 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <ImageView - android:id="@id/icon" - android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/gpodder_icon" /> + android:orientation="vertical"> - <TextView - android:id="@id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_login_descr" - android:layout_below="@id/icon" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginBottom="8dp"> - <EditText - android:id="@+id/etxtUsername" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/username_label" - android:layout_below="@id/txtvDescription" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:maxLines="1" - android:inputType="text" - android:imeOptions="actionNext|flagNoFullscreen" - android:nextFocusForward="@id/etxtPassword"/> + <ImageView + android:layout_width="64dp" + android:layout_height="64dp" + android:src="@drawable/gpodder_icon"/> - <EditText - android:id="@+id/etxtPassword" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/password_label" - android:layout_below="@id/etxtUsername" - android:inputType="textPassword" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:imeOptions="actionGo|flagNoFullscreen" - android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> + <TextView + android:id="@+id/createAccountButton" + android:layout_width="0dp" + android:textAlignment="textEnd" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:textColor="?colorAccent" + android:layout_weight="1" + android:layout_gravity="center_vertical|end" + android:text="@string/create_account"/> + </LinearLayout> - <Button - android:id="@+id/butLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:text="@string/gpodnetauth_login_butLabel"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_toLeftOf="@id/butLogin" - android:layout_toStartOf="@id/butLogin" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_small" - android:maxLines="2" - android:ellipsize="end" - android:gravity="center" - android:layout_margin="16dp" - tools:text="Error message" - tools:background="@android:color/holo_green_dark" /> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <ProgressBar - android:id="@+id/progBarLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:layout_alignTop="@+id/butLogin" - android:layout_toLeftOf="@+id/butLogin" - android:layout_toStartOf="@+id/butLogin"/> + </com.google.android.material.textfield.TextInputLayout> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary" - android:layout_marginTop="16dp" - android:text="@string/gpodnetauth_login_register" - android:autoLink="web" - android:layout_below="@id/butLogin"/> -</RelativeLayout>
\ No newline at end of file + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:inputType="textPassword" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" + android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> + + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="end|center_vertical"> + + <TextView + android:id="@+id/credentialsError" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_small" + android:maxLines="2" + android:ellipsize="end" + android:gravity="center" + tools:text="Error message" + tools:background="@android:color/holo_green_dark"/> + + <ProgressBar + android:id="@+id/progBarLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="right"/> + + <Button + android:id="@+id/butLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_login_butLabel"/> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_device.xml b/app/src/main/res/layout/gpodnetauth_device.xml index 7837121e1..656ba0889 100644 --- a/app/src/main/res/layout/gpodnetauth_device.xml +++ b/app/src/main/res/layout/gpodnetauth_device.xml @@ -1,114 +1,61 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_title" - android:layout_alignParentTop="true" - android:layout_marginBottom="16dp" - style="@style/AntennaPod.TextView.Heading"/> + android:orientation="vertical"> - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> - - <EditText - android:id="@+id/etxtCaption" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/gpodnetauth_device_caption" - android:layout_below="@id/txtvDescription" - android:imeOptions="flagNoFullscreen"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/txtvDeviceID" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_deviceID" - android:textSize="@dimen/text_size_medium" - android:layout_below="@id/etxtCaption"/> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/deviceName" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_device_name" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <EditText - android:id="@+id/etxtDeviceID" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvDeviceID" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:imeOptions="flagNoFullscreen"/> + </com.google.android.material.textfield.TextInputLayout> <Button - android:id="@+id/butCreateNewDevice" - android:layout_width="wrap_content" + android:id="@+id/createDeviceButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_create_device"/> + + <TextView + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:layout_below="@id/etxtDeviceID" - android:text="@string/gpodnetauth_device_butCreateNewDevice"/> + style="@style/AntennaPod.TextView.Heading" + android:layout_marginTop="16dp" + android:text="@string/gpodnetauth_existing_devices"/> <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" + android:id="@+id/deviceSelectError" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_below="@id/etxtDeviceID" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" android:textColor="@color/download_failed_red" android:textSize="@dimen/text_size_small" + android:visibility="gone" tools:text="Error message" tools:background="@android:color/holo_green_dark" /> - <ProgressBar - android:id="@+id/progbarCreateDevice" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignTop="@id/butCreateNewDevice" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_medium" - android:visibility="gone" - /> - - <TextView - android:id="@+id/txtvChooseExistingDevice" + <LinearLayout + android:id="@+id/devicesContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_chooseExistingDevice" - android:layout_below="@id/butCreateNewDevice" - android:textColor="?android:attr/textColorPrimary" - android:textSize="@dimen/text_size_medium" - android:layout_marginTop="32dp"/> + android:orientation="vertical" /> - <Button - android:id="@+id/butChooseExistingDevice" + <ProgressBar + android:id="@+id/progbarCreateDevice" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_butChoose" - android:layout_below="@+id/spinnerChooseDevice" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true"/> - - <Spinner - android:id="@+id/spinnerChooseDevice" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvChooseExistingDevice" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true"/> + android:textColor="@color/download_failed_red" + android:visibility="gone" /> -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_device_row.xml b/app/src/main/res/layout/gpodnetauth_device_row.xml new file mode 100644 index 000000000..d39c00571 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_device_row.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp"> + + <Button + android:id="@+id/selectDeviceButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?attr/materialButtonOutlinedStyle" /> +</FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_dialog.xml b/app/src/main/res/layout/gpodnetauth_dialog.xml new file mode 100644 index 000000000..a70b76a49 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_dialog.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:clipToPadding="false"> + <ViewFlipper + android:id="@+id/viewflipper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inAnimation="@anim/slide_right_in" + android:outAnimation="@anim/slide_left_out"> + + <include layout="@layout/gpodnetauth_host" /> + <include layout="@layout/gpodnetauth_credentials" /> + <include layout="@layout/gpodnetauth_device" /> + <include layout="@layout/gpodnetauth_finish" /> + + </ViewFlipper> +</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_finish.xml b/app/src/main/res/layout/gpodnetauth_finish.xml index fdaa0d5d0..f0bcfd4dc 100644 --- a/app/src/main/res/layout/gpodnetauth_finish.xml +++ b/app/src/main/res/layout/gpodnetauth_finish.xml @@ -1,46 +1,28 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> + android:layout_height="wrap_content" + android:orientation="vertical"> <ImageView android:id="@id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="64dp" + android:layout_height="64dp" android:src="@drawable/gpodder_icon" /> <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/icon" - android:text="@string/gpodnetauth_finish_title" - style="@style/AntennaPod.TextView.Heading"/> - - <TextView android:id="@+id/txtvDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/gpodnetauth_finish_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" android:textColor="?android:attr/textColorPrimary" /> <Button android:id="@+id/butSyncNow" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvDescription" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:text="@string/gpodnetauth_finish_butsyncnow"/> - <Button - android:id="@+id/butGoMainscreen" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/butSyncNow" - android:text="@string/gpodnetauth_finish_butgomainscreen"/> - -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_host.xml b/app/src/main/res/layout/gpodnetauth_host.xml new file mode 100644 index 000000000..52c5fdb5d --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_host.xml @@ -0,0 +1,50 @@ +<?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="wrap_content" + android:orientation="vertical"> + + <RadioGroup + android:id="@+id/serverRadioGroup" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <RadioButton + android:id="@+id/officialServerRadio" + android:text="@string/gpodnetauth_server_official" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:checked="true"/> + <RadioButton + android:id="@+id/customServerRadio" + android:text="@string/gpodnetauth_server_custom" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + </RadioGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/serverUrlTextInput" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/serverUrlText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_host" + android:inputType="textNoSuggestions" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/chooseHostButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_select_server"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/onlinefeedview_activity.xml b/app/src/main/res/layout/onlinefeedview_activity.xml index 88ffbb8b6..909d676f0 100644 --- a/app/src/main/res/layout/onlinefeedview_activity.xml +++ b/app/src/main/res/layout/onlinefeedview_activity.xml @@ -105,35 +105,55 @@ tools:text="Podcast author"/> </RelativeLayout> - <Spinner - android:id="@+id/alternate_urls_spinner" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginRight="8dp" - android:layout_marginTop="8dp" - android:padding="8dp" - android:textColor="?android:attr/textColorPrimary" - android:textSize="@dimen/text_size_micro"/> - - <Button - android:id="@+id/subscribeButton" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:focusable="false" - android:text="@string/subscribe_label"/> - - <Button - android:id="@+id/stopPreviewButton" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" android:layout_marginBottom="16dp" - android:focusable="false" - android:text="@string/stop_preview" - android:visibility="gone" /> + android:orientation="vertical"> + + <Spinner + android:id="@+id/alternate_urls_spinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:padding="8dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="@dimen/text_size_micro" /> + + <Button + android:id="@+id/subscribeButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/subscribe_label" /> + + <CheckBox + android:id="@+id/autoDownloadCheckBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:focusable="false" + android:checked="true" + android:text="@string/auto_download_label" + android:visibility="gone" + tools:visibility="visible" /> + + <Button + android:id="@+id/stopPreviewButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:focusable="false" + android:text="@string/stop_preview" + android:visibility="gone" + tools:visibility="visible" /> + + </LinearLayout> <ListView android:id="@+id/listView" diff --git a/app/src/main/res/layout/quick_feed_discovery.xml b/app/src/main/res/layout/quick_feed_discovery.xml index 0c55311e3..9ef3db180 100644 --- a/app/src/main/res/layout/quick_feed_discovery.xml +++ b/app/src/main/res/layout/quick_feed_discovery.xml @@ -23,7 +23,7 @@ <Button android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minHeight="0dp" + android:minHeight="48dp" android:minWidth="0dp" android:text="@string/discover_more" style="@style/Widget.MaterialComponents.Button.TextButton" @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <de.danoeh.antennapod.view.WrappingGridView + <de.danoeh.antennapod.ui.common.WrappingGridView android:id="@+id/discover_grid" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -85,6 +85,7 @@ </RelativeLayout> <TextView + android:id="@+id/discover_powered_by_itunes" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="?android:attr/textColorTertiary" diff --git a/app/src/main/res/layout/quick_feed_discovery_item.xml b/app/src/main/res/layout/quick_feed_discovery_item.xml index e1c91f9d8..cb03b6677 100644 --- a/app/src/main/res/layout/quick_feed_discovery_item.xml +++ b/app/src/main/res/layout/quick_feed_discovery_item.xml @@ -7,7 +7,7 @@ android:padding="4dp" android:clipToPadding="false"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/discovery_cover" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/app/src/main/res/layout/searchlist_item_feed.xml b/app/src/main/res/layout/searchlist_item_feed.xml index 607a3197f..c16911f99 100644 --- a/app/src/main/res/layout/searchlist_item_feed.xml +++ b/app/src/main/res/layout/searchlist_item_feed.xml @@ -7,7 +7,7 @@ android:padding="4dp" android:clipToPadding="false"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/discovery_cover" android:layout_width="match_parent" android:layout_height="96dp" diff --git a/app/src/main/res/layout/secondary_action.xml b/app/src/main/res/layout/secondary_action.xml index 73ca174a6..e5bff480e 100644 --- a/app/src/main/res/layout/secondary_action.xml +++ b/app/src/main/res/layout/secondary_action.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginRight="12dp" @@ -19,9 +20,10 @@ tools:ignore="ContentDescription" tools:src="@sample/secondaryaction"/> - <de.danoeh.antennapod.view.CircularProgressBar + <de.danoeh.antennapod.ui.common.CircularProgressBar android:id="@+id/secondaryActionProgress" android:layout_width="40dp" android:layout_gravity="center" - android:layout_height="40dp"/> + android:layout_height="40dp" + app:foregroundColor="?attr/action_icon_color"/> </FrameLayout> diff --git a/app/src/main/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index e0c821868..7fa738f12 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -8,7 +8,7 @@ android:layout_height="match_parent" android:foreground="?attr/selectableItemBackground"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/imgvCover" android:layout_width="fill_parent" android:layout_height="fill_parent" diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml index 6523bb1be..6b6ab3195 100644 --- a/app/src/main/res/layout/time_dialog.xml +++ b/app/src/main/res/layout/time_dialog.xml @@ -2,6 +2,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:gravity="center" android:padding="16dp"> @@ -52,10 +53,11 @@ <TextView android:text="00:00:00" android:layout_gravity="center" + android:gravity="center" android:textSize="32sp" android:textColor="?android:attr/textColorPrimary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_height="match_parent" android:id="@+id/time"/> <Button @@ -64,6 +66,45 @@ android:layout_height="wrap_content" android:id="@+id/disableSleeptimerButton"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/extendSleepFiveMinutesButton" + style="?attr/materialButtonOutlinedStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="4dp" + android:layout_marginRight="4dp" + android:layout_weight="1" + android:padding="5dp" + tools:text="+5 min" /> + + <Button + android:id="@+id/extendSleepTenMinutesButton" + style="?attr/materialButtonOutlinedStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_weight="1" + tools:text="+10 min" /> + + <Button + android:id="@+id/extendSleepTwentyMinutesButton" + style="?attr/materialButtonOutlinedStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginRight="4dp" + android:layout_marginLeft="4dp" + android:layout_weight="1" + tools:text="+20 min" /> + + </LinearLayout> + </LinearLayout> @@ -93,4 +134,4 @@ </LinearLayout> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/app/src/main/res/layout/videoplayer_activity.xml b/app/src/main/res/layout/videoplayer_activity.xml index c978a1e4d..e0632ef41 100644 --- a/app/src/main/res/layout/videoplayer_activity.xml +++ b/app/src/main/res/layout/videoplayer_activity.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" android:background="@color/black" android:orientation="vertical" android:id="@+id/videoframe"> @@ -75,6 +76,33 @@ android:layout_gravity="bottom|center" android:orientation="vertical"> + <androidx.cardview.widget.CardView + android:id="@+id/cardViewSeek" + android:alpha="0" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="12dp" + android:layout_gravity="center" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?attr/seek_background" + app:cardElevation="0dp" + tools:alpha="1"> + + <TextView + android:id="@+id/txtvSeek" + android:gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="24dp" + android:paddingTop="4dp" + android:paddingRight="24dp" + android:paddingBottom="4dp" + android:textColor="@color/white" + android:textSize="24sp" + tools:text="1:06:29" /> + + </androidx.cardview.widget.CardView> + <RelativeLayout android:id="@+id/timecontrol" android:layout_width="match_parent" diff --git a/app/src/main/res/menu/bug_report_options.xml b/app/src/main/res/menu/bug_report_options.xml new file mode 100644 index 000000000..62963210c --- /dev/null +++ b/app/src/main/res/menu/bug_report_options.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:id="@+id/export_logcat" + android:title="@string/export_logs_menu_title" /> + +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml index b1daf1f36..a5fbe0c20 100644 --- a/app/src/main/res/menu/feedinfo.xml +++ b/app/src/main/res/menu/feedinfo.xml @@ -8,17 +8,25 @@ android:title="@string/visit_website_label" android:visible="true"/> <item - android:id="@+id/share_link_item" - custom:showAsAction="collapseActionView" - android:title="@string/share_website_url_label"/> - <item - android:id="@+id/share_download_url_item" - custom:showAsAction="collapseActionView" - android:title="@string/share_feed_url_label"/> + android:id="@+id/share_parent" + custom:showAsAction="ifRoom" + android:title="@string/share_label_with_ellipses" + android:icon="?attr/ic_share" + android:visible="true"> + <menu android:id="@+id/share_submenu"> + <item + android:id="@+id/share_link_item" + custom:showAsAction="collapseActionView" + android:title="@string/share_website_url_label"/> + <item + android:id="@+id/share_download_url_item" + custom:showAsAction="collapseActionView" + android:title="@string/share_feed_url_label"/> + </menu> + </item> <item android:id="@+id/reconnect_local_folder" custom:showAsAction="collapseActionView" android:title="@string/reconnect_local_folder" android:visible="false" /> - -</menu> +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/subscriptions.xml b/app/src/main/res/menu/subscriptions.xml index b7dc95299..99acc4bb6 100644 --- a/app/src/main/res/menu/subscriptions.xml +++ b/app/src/main/res/menu/subscriptions.xml @@ -23,22 +23,21 @@ android:title="@string/subscription_num_columns" custom:showAsAction="never"> <menu> - <item - android:id="@+id/subscription_num_columns_2" - android:checkable="true" - android:title="2"/> - <item - android:id="@+id/subscription_num_columns_3" - android:checkable="true" - android:title="3"/> - <item - android:id="@+id/subscription_num_columns_4" - android:checkable="true" - android:title="4"/> - <item - android:id="@+id/subscription_num_columns_5" - android:checkable="true" - android:title="5"/> + <group + android:checkableBehavior="single"> + <item + android:id="@+id/subscription_num_columns_2" + android:title="2"/> + <item + android:id="@+id/subscription_num_columns_3" + android:title="3"/> + <item + android:id="@+id/subscription_num_columns_4" + android:title="4"/> + <item + android:id="@+id/subscription_num_columns_5" + android:title="5"/> + </group> </menu> </item> </menu> diff --git a/app/src/main/res/xml/actions.xml b/app/src/main/res/xml/actions.xml new file mode 100644 index 000000000..20dc3dc9b --- /dev/null +++ b/app/src/main/res/xml/actions.xml @@ -0,0 +1,25 @@ +<?xml version ="1.0" encoding ="utf-8"?> +<actions> + <action intentName="actions.intent.OPEN_APP_FEATURE"> + <fulfillment urlTemplate="https://antennapod.org/deeplink/main{?page}"> + <parameter-mapping intentParameter="feature" urlParameter="page" /> + </fulfillment> + <parameter name="feature"> + <entity-set-reference entitySetId="featureEntitySet" /> + </parameter> + </action> + + <action intentName="actions.intent.GET_THING"> + <fulfillment urlTemplate="https://antennapod.org/deeplink/search{?query}"> + <parameter-mapping intentParameter="thing.name" urlParameter="query"/> + </fulfillment> + </action> + + <entity-set entitySetId="featureEntitySet"> + <entity identifier="QUEUE" name="@string/queue_label" /> + <entity identifier="EPISODES" name="@string/episodes_label" /> + <entity identifier="DOWNLOADS" name="@string/downloads_label" /> + <entity identifier="SUBSCRIPTIONS" name="@string/subscriptions_label" /> + <entity identifier="HISTORY" name="@string/playback_history_label" /> + </entity-set> +</actions> diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index 9d5ed5e8b..13288fbda 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -9,6 +9,14 @@ android:title="@string/keep_updated" android:summary="@string/keep_updated_summary"/> + <SwitchPreferenceCompat + android:key="episodeNotification" + android:defaultValue="false" + android:dependency="keepUpdated" + android:icon="?attr/ic_notifications" + android:title="@string/episode_notification" + android:summary="@string/episode_notification_summary"/> + <Preference android:key="authentication" android:icon="?attr/ic_key" diff --git a/app/src/main/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml index 79cdd4a69..803cc89ed 100644 --- a/app/src/main/res/xml/player_widget_info.xml +++ b/app/src/main/res/xml/player_widget_info.xml @@ -8,5 +8,4 @@ android:minWidth="250dp" android:minResizeWidth="40dp" android:configure="de.danoeh.antennapod.activity.WidgetConfigActivity"> - -</appwidget-provider>
\ No newline at end of file +</appwidget-provider> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 1630dc2f9..805dff47d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -53,8 +53,8 @@ android:key="project" android:title="@string/project_pref"> <Preference - android:key="prefFaq" - android:title="@string/pref_faq" + android:key="prefDocumentation" + android:title="@string/documentation_support" android:icon="?attr/ic_questionmark" /> <Preference android:key="prefViewForum" diff --git a/app/src/main/res/xml/preferences_about.xml b/app/src/main/res/xml/preferences_about.xml index 475a1152f..f56b7f2ac 100644 --- a/app/src/main/res/xml/preferences_about.xml +++ b/app/src/main/res/xml/preferences_about.xml @@ -17,7 +17,7 @@ <Preference android:key="about_privacy_policy" android:icon="?attr/ic_questionmark" - android:summary="https://antennapod.org/privacy.html" + android:summary="www.antennapod.org/privacy" android:title="@string/privacy_policy"/> <Preference android:key="about_licenses" diff --git a/app/src/main/res/xml/preferences_gpodder.xml b/app/src/main/res/xml/preferences_gpodder.xml index 7bddbf245..a210b8e11 100644 --- a/app/src/main/res/xml/preferences_gpodder.xml +++ b/app/src/main/res/xml/preferences_gpodder.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - - <PreferenceScreen + <Preference + android:key="pref_gpodnet_description" + android:icon="@drawable/gpodder_icon" + android:summary="@string/gpodnet_description"/> + <Preference android:key="pref_gpodnet_authenticate" android:title="@string/pref_gpodnet_authenticate_title" - android:summary="@string/pref_gpodnet_authenticate_sum"> - <intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - </PreferenceScreen> + android:summary="@string/pref_gpodnet_authenticate_sum"/> <Preference android:key="pref_gpodnet_setlogin_information" android:title="@string/pref_gpodnet_setlogin_information_title" @@ -23,8 +24,5 @@ <Preference android:key="pref_gpodnet_logout" android:title="@string/pref_gpodnet_logout_title"/> - <Preference - android:key="pref_gpodnet_hostname" - android:title="@string/pref_gpodnet_sethostname_title"/> </PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_notifications.xml b/app/src/main/res/xml/preferences_notifications.xml index 3aa907ca8..237ea5c99 100644 --- a/app/src/main/res/xml/preferences_notifications.xml +++ b/app/src/main/res/xml/preferences_notifications.xml @@ -2,21 +2,28 @@ <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - <SwitchPreferenceCompat - android:defaultValue="true" - android:enabled="true" - android:key="prefShowDownloadReport" - android:summary="@string/pref_showDownloadReport_sum" - android:title="@string/pref_showDownloadReport_title" /> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefShowAutoDownloadReport" - android:summary="@string/pref_showAutoDownloadReport_sum" - android:title="@string/pref_showAutoDownloadReport_title" /> - <SwitchPreferenceCompat - android:defaultValue="true" - android:key="pref_gpodnet_notifications" - android:summary="@string/pref_gpodnet_notifications_sum" - android:title="@string/pref_gpodnet_notifications_title" /> + <PreferenceCategory + android:title="@string/notification_group_news"> + <SwitchPreferenceCompat + android:defaultValue="false" + android:enabled="true" + android:key="prefShowAutoDownloadReport" + android:summary="@string/notification_channel_episode_auto_download" + android:title="@string/notification_channel_auto_download" /> + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/notification_group_errors"> + <SwitchPreferenceCompat + android:defaultValue="true" + android:enabled="true" + android:key="prefShowDownloadReport" + android:summary="@string/notification_channel_download_error_description" + android:title="@string/notification_channel_download_error" /> + <SwitchPreferenceCompat + android:defaultValue="true" + android:key="pref_gpodnet_notifications" + android:summary="@string/notification_channel_sync_error_description" + android:title="@string/notification_channel_sync_error" /> + </PreferenceCategory> </PreferenceScreen>
\ No newline at end of file diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index d2999c59d..2be8492eb 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -44,18 +44,6 @@ </PreferenceCategory> <PreferenceCategory android:title="@string/playback_control"> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefHardwareForwardButtonSkips" - android:summary="@string/pref_hardwareForwardButtonSkips_sum" - android:title="@string/pref_hardwareForwardButtonSkips_title"/> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefHardwarePreviousButtonRestarts" - android:summary="@string/pref_hardwarePreviousButtonRestarts_sum" - android:title="@string/pref_hardwarePreviousButtonRestarts_title"/> <Preference android:key="prefPlaybackFastForwardDeltaLauncher" android:summary="@string/pref_fast_forward_sum" @@ -80,6 +68,23 @@ android:title="@string/pref_stream_over_download_title"/> </PreferenceCategory> + <PreferenceCategory android:title="@string/reassign_hardware_buttons"> + <ListPreference + android:defaultValue="@string/keycode_media_fast_forward" + android:entries="@array/button_action_options" + android:entryValues="@array/button_action_values" + android:key="prefHardwareForwardButton" + android:title="@string/pref_hardware_forward_button_title" + android:summary="@string/pref_hardware_forward_button_summary"/> + <ListPreference + android:defaultValue="@string/keycode_media_rewind" + android:entries="@array/button_action_options" + android:entryValues="@array/button_action_values" + android:key="prefHardwarePreviousButton" + android:title="@string/pref_hardware_previous_button_title" + android:summary="@string/pref_hardware_previous_button_summary"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/queue_label"> <SwitchPreferenceCompat android:defaultValue="true" diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index 203e14d42..f8e80cdff 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen - xmlns:android="http://schemas.android.com/apk/res/android"> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> <PreferenceCategory android:title="@string/appearance"> <ListPreference @@ -20,6 +21,12 @@ android:summary="@string/pref_episode_cover_summary" android:defaultValue="true" android:enabled="true"/> + <SwitchPreferenceCompat + android:title="@string/pref_show_remain_time_title" + android:key="showTimeLeft" + android:summary="@string/pref_show_remain_time_summary" + android:defaultValue="false" + android:enabled="true"/> </PreferenceCategory> <PreferenceCategory android:title="@string/subscriptions_label"> <Preference @@ -44,7 +51,8 @@ android:enabled="true" android:key="prefExpandNotify" android:summary="@string/pref_expandNotify_sum" - android:title="@string/pref_expandNotify_title"/> + android:title="@string/pref_expandNotify_title" + search:ignore="true"/> <SwitchPreferenceCompat android:defaultValue="true" android:enabled="true" diff --git a/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java b/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java index cfadf0772..66072e2fa 100644 --- a/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java +++ b/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import androidx.annotation.VisibleForTesting; import android.util.Log; -import android.widget.Toast; import java.lang.ref.WeakReference; import java.util.concurrent.TimeUnit; |