diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2014-10-24 20:40:07 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2014-10-24 20:40:07 +0200 |
commit | cc052e91ad8a87b00b93649ec0f6a06bcae6267a (patch) | |
tree | 12cacac4fb5c94af2955812a3167eefb325f286d /app/src/androidTest/java/de/test/antennapod | |
parent | baa7d5f11283cb7668d45b561af5d38f0ccb9632 (diff) | |
parent | b5066d02b4acf31da093190a1a57a9d961bb04ca (diff) | |
download | AntennaPod-cc052e91ad8a87b00b93649ec0f6a06bcae6267a.zip |
Merge branch 'migration' into develop
Non-GUI classes have been moved into the 'core' project in order to allow AntennaPod SP to reference it as a subproject.
Conflicts:
app/src/main/AndroidManifest.xml
build.gradle
core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java
core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
gradle/wrapper/gradle-wrapper.properties
pom.xml
Diffstat (limited to 'app/src/androidTest/java/de/test/antennapod')
27 files changed, 6638 insertions, 0 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java b/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java new file mode 100644 index 000000000..24cd6e669 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java @@ -0,0 +1,16 @@ +package de.test.antennapod; + +import android.test.InstrumentationTestRunner; +import android.test.suitebuilder.TestSuiteBuilder; +import junit.framework.TestSuite; + +public class AntennaPodTestRunner extends InstrumentationTestRunner { + + @Override + public TestSuite getAllTests() { + return new TestSuiteBuilder(AntennaPodTestRunner.class) + .includeAllPackagesUnderHere() + .excludePackages("de.test.antennapod.gpodnet") + .build(); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java new file mode 100644 index 000000000..14a3b27b0 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java @@ -0,0 +1,114 @@ +package de.test.antennapod.gpodnet; + +import android.test.AndroidTestCase; + +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Test class for GpodnetService + */ +public class GPodnetServiceTest extends AndroidTestCase { + + private GpodnetService service; + + private static final String USER = ""; + private static final String PW = ""; + + @Override + protected void setUp() throws Exception { + super.setUp(); + service = new GpodnetService(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void authenticate() throws GpodnetServiceException { + service.authenticate(USER, PW); + } + + public void testUploadSubscription() throws GpodnetServiceException { + authenticate(); + ArrayList<String> l = new ArrayList<String>(); + l.add("http://bitsundso.de/feed"); + service.uploadSubscriptions(USER, "radio", l); + } + + public void testUploadSubscription2() throws GpodnetServiceException { + authenticate(); + ArrayList<String> l = new ArrayList<String>(); + l.add("http://bitsundso.de/feed"); + l.add("http://gamesundso.de/feed"); + service.uploadSubscriptions(USER, "radio", l); + } + + public void testUploadChanges() throws GpodnetServiceException { + authenticate(); + String[] URLS = {"http://bitsundso.de/feed", "http://gamesundso.de/feed", "http://cre.fm/feed/mp3/", "http://freakshow.fm/feed/m4a/"}; + List<String> subscriptions = Arrays.asList(URLS[0], URLS[1]); + List<String> removed = Arrays.asList(URLS[0]); + List<String> added = Arrays.asList(URLS[2], URLS[3]); + service.uploadSubscriptions(USER, "radio", subscriptions); + service.uploadChanges(USER, "radio", added, removed); + } + + public void testGetSubscriptionChanges() throws GpodnetServiceException { + authenticate(); + service.getSubscriptionChanges(USER, "radio", 1362322610L); + } + + public void testGetSubscriptionsOfUser() + throws GpodnetServiceException { + authenticate(); + service.getSubscriptionsOfUser(USER); + } + + public void testGetSubscriptionsOfDevice() + throws GpodnetServiceException { + authenticate(); + service.getSubscriptionsOfDevice(USER, "radio"); + } + + public void testConfigureDevices() throws GpodnetServiceException { + authenticate(); + service.configureDevice(USER, "foo", "This is an updated caption", + GpodnetDevice.DeviceType.LAPTOP); + } + + public void testGetDevices() throws GpodnetServiceException { + authenticate(); + service.getDevices(USER); + } + + public void testGetSuggestions() throws GpodnetServiceException { + authenticate(); + service.getSuggestions(10); + } + + public void testTags() throws GpodnetServiceException { + service.getTopTags(20); + } + + public void testPodcastForTags() throws GpodnetServiceException { + List<GpodnetTag> tags = service.getTopTags(20); + service.getPodcastsForTag(tags.get(1), + 10); + } + + public void testSearch() throws GpodnetServiceException { + service.searchPodcasts("linux", 64); + } + + public void testToplist() throws GpodnetServiceException { + service.getPodcastToplist(10); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java new file mode 100644 index 000000000..52067c971 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java @@ -0,0 +1,176 @@ +package de.test.antennapod.handler; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator; +import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; +import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; +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; + +/** + * Tests for FeedHandler + */ +public class FeedHandlerTest extends InstrumentationTestCase { + private static final String FEEDS_DIR = "testfeeds"; + + File file = null; + OutputStream outputStream = null; + + protected void setUp() throws Exception { + super.setUp(); + Context context = getInstrumentation().getContext(); + 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); + } + + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + file.delete(); + file = null; + + outputStream.close(); + outputStream = null; + } + + private 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; + } + + private 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()); + + if (feed.getImage() != null) { + FeedImage image = feed.getImage(); + FeedImage parsedImage = parsedFeed.getImage(); + assertNotNull(parsedImage); + + assertEquals(image.getTitle(), parsedImage.getTitle()); + assertEquals(image.getDownload_url(), parsedImage.getDownload_url()); + } + + 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()); + } + + if (item.hasItemImage()) { + assertTrue(parsedItem.hasItemImage()); + FeedImage image = item.getImage(); + FeedImage parsedImage = parsedItem.getImage(); + + assertEquals(image.getTitle(), parsedImage.getTitle()); + assertEquals(image.getDownload_url(), parsedImage.getDownload_url()); + } + + 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()); + } + } + } + } + } + + public void testRSS2Basic() throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { + Feed f1 = createTestFeed(10, false, true, true); + Feed f2 = runFeedTest(f1, new RSS2Generator(), "UTF-8", RSS2Generator.FEATURE_WRITE_GUID); + feedValid(f1, f2, Feed.TYPE_RSS2); + } + + public void testAtomBasic() throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { + Feed f1 = createTestFeed(10, false, true, true); + Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0); + feedValid(f1, f2, Feed.TYPE_ATOM1); + } + + private Feed createTestFeed(int numItems, boolean withImage, boolean withFeedMedia, boolean withChapters) { + FeedImage image = null; + if (withImage) { + image = new FeedImage(0, "image", null, "http://example.com/picture", false); + } + Feed feed = new Feed(0, new Date(), "title", "http://example.com", "This is the description", + "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", image, file.getAbsolutePath(), + "http://example.com/feed", true); + feed.setItems(new ArrayList<FeedItem>()); + + 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), false, feed); + feed.getItems().add(item); + if (withFeedMedia) { + item.setMedia(new FeedMedia(0, item, 4711, 0, 100, "audio/mp3", null, "http://example.com/media-" + i, + false, null, 0)); + } + } + + return feed; + } + +} diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java new file mode 100644 index 000000000..1a561f282 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -0,0 +1,170 @@ +package de.test.antennapod.service.download; + +import android.test.InstrumentationTestCase; +import android.util.Log; +import de.danoeh.antennapod.core.feed.FeedFile; +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.service.download.HttpDownloader; +import de.danoeh.antennapod.core.util.DownloadError; +import de.test.antennapod.util.service.download.HTTPBin; + +import java.io.File; +import java.io.IOException; + +public class HttpDownloaderTest extends InstrumentationTestCase { + private static final String TAG = "HttpDownloaderTest"; + private static final String DOWNLOAD_DIR = "testdownloads"; + + private static boolean successful = true; + + private File destDir; + + private HTTPBin httpServer; + + public HttpDownloaderTest() { + super(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + File[] contents = destDir.listFiles(); + for (File f : contents) { + assertTrue(f.delete()); + } + + httpServer.stop(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + destDir = getInstrumentation().getTargetContext().getExternalFilesDir(DOWNLOAD_DIR); + assertNotNull(destDir); + assertTrue(destDir.exists()); + httpServer = new HTTPBin(); + httpServer.start(); + } + + private FeedFileImpl setupFeedFile(String downloadUrl, String title, boolean deleteExisting) { + FeedFileImpl feedfile = new FeedFileImpl(downloadUrl); + String fileUrl = new File(destDir, title).getAbsolutePath(); + File file = new File(fileUrl); + if (deleteExisting) { + Log.d(TAG, "Deleting file: " + file.delete()); + } + feedfile.setFile_url(fileUrl); + return feedfile; + } + + private Downloader download(String url, String title, boolean expectedResult) { + return download(url, title, expectedResult, true, null, null, true); + } + + private Downloader download(String url, String title, boolean expectedResult, boolean deleteExisting, String username, String password, boolean deleteOnFail) { + FeedFile feedFile = setupFeedFile(url, title, deleteExisting); + DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail); + Downloader downloader = new HttpDownloader(request); + downloader.call(); + DownloadStatus status = downloader.getResult(); + assertNotNull(status); + assertTrue(status.isSuccessful() == expectedResult); + assertTrue(status.isDone()); + // the file should not exist if the download has failed and deleteExisting was true + assertTrue(!deleteExisting || new File(feedFile.getFile_url()).exists() == expectedResult); + return downloader; + } + + + private static final String URL_404 = HTTPBin.BASE_URL + "/status/404"; + private static final String URL_AUTH = HTTPBin.BASE_URL + "/basic-auth/user/passwd"; + + public void testPassingHttp() { + download(HTTPBin.BASE_URL + "/status/200", "test200", true); + } + + public void testRedirect() { + download(HTTPBin.BASE_URL + "/redirect/4", "testRedirect", true); + } + + public void testGzip() { + download("http://httpbin.org/gzip", "testGzip", true); + } + + public void test404() { + download(URL_404, "test404", false); + } + + public void testCancel() { + final String url = HTTPBin.BASE_URL + "/delay/3"; + FeedFileImpl feedFile = setupFeedFile(url, "delay", true); + final Downloader downloader = new HttpDownloader(new DownloadRequest(feedFile.getFile_url(), url, "delay", 0, feedFile.getTypeAsInt())); + Thread t = new Thread() { + @Override + public void run() { + downloader.call(); + } + }; + t.start(); + downloader.cancel(); + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + DownloadStatus result = downloader.getResult(); + assertTrue(result.isDone()); + assertFalse(result.isSuccessful()); + assertTrue(result.isCancelled()); + assertFalse(new File(feedFile.getFile_url()).exists()); + } + + public void testDeleteOnFailShouldDelete() { + Downloader downloader = download(URL_404, "testDeleteOnFailShouldDelete", false, true, null, null, true); + assertFalse(new File(downloader.getDownloadRequest().getDestination()).exists()); + } + + public void testDeleteOnFailShouldNotDelete() throws IOException { + String filename = "testDeleteOnFailShouldDelete"; + File dest = new File(destDir, filename); + dest.delete(); + assertTrue(dest.createNewFile()); + Downloader downloader = download(URL_404, filename, false, false, null, null, false); + assertTrue(new File(downloader.getDownloadRequest().getDestination()).exists()); + } + + public void testAuthenticationShouldSucceed() throws InterruptedException { + download(URL_AUTH, "testAuthSuccess", true, true, "user", "passwd", true); + } + + public void testAuthenticationShouldFail() { + Downloader downloader = download(URL_AUTH, "testAuthSuccess", false, true, "user", "Wrong passwd", true); + assertEquals(DownloadError.ERROR_UNAUTHORIZED, downloader.getResult().getReason()); + } + + /* TODO: replace with smaller test file + public void testUrlWithSpaces() { + download("http://acedl.noxsolutions.com/ace/Don't Call Salman Rushdie Sneezy in Finland.mp3", "testUrlWithSpaces", true); + } + */ + + private static class FeedFileImpl extends FeedFile { + public FeedFileImpl(String download_url) { + super(null, download_url, false); + } + + + @Override + public String getHumanReadableIdentifier() { + return download_url; + } + + @Override + public int getTypeAsInt() { + return 0; + } + } + +} diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java new file mode 100644 index 000000000..aac4c245a --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -0,0 +1,1177 @@ +package de.test.antennapod.service.playback; + +import android.content.Context; +import android.media.RemoteControlClient; +import android.test.InstrumentationTestCase; +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.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.test.antennapod.util.service.download.HTTPBin; +import junit.framework.AssertionFailedError; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceMediaPlayer + */ +public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { + private static final String TAG = "PlaybackServiceMediaPlayerTest"; + + private static final String PLAYABLE_FILE_URL = "http://127.0.0.1:" + HTTPBin.PORT + "/files/0"; + private static final String PLAYABLE_DEST_URL = "psmptestfile.mp3"; + private String PLAYABLE_LOCAL_URL = null; + private static final int LATCH_TIMEOUT_SECONDS = 10; + + private HTTPBin httpServer; + + private volatile AssertionFailedError assertionError; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + httpServer.stop(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + assertionError = null; + + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + + httpServer = new HTTPBin(); + httpServer.start(); + + File cacheDir = context.getExternalFilesDir("testFiles"); + if (cacheDir == null) + cacheDir = context.getExternalFilesDir("testFiles"); + File dest = new File(cacheDir, PLAYABLE_DEST_URL); + + assertNotNull(cacheDir); + assertTrue(cacheDir.canWrite()); + assertTrue(cacheDir.canRead()); + if (!dest.exists()) { + InputStream i = getInstrumentation().getContext().getAssets().open("testfile.mp3"); + OutputStream o = new FileOutputStream(new File(cacheDir, PLAYABLE_DEST_URL)); + IOUtils.copy(i, o); + o.flush(); + o.close(); + i.close(); + } + PLAYABLE_LOCAL_URL = "file://" + dest.getAbsolutePath(); + assertEquals(0, httpServer.serveFile(dest)); + } + + private void checkPSMPInfo(PlaybackServiceMediaPlayer.PSMPInfo info) { + try { + switch (info.playerStatus) { + case PLAYING: + case PAUSED: + case PREPARED: + case PREPARING: + case INITIALIZED: + case INITIALIZING: + case SEEKING: + assertNotNull(info.playable); + break; + case STOPPED: + assertNull(info.playable); + break; + case ERROR: + assertNull(info.playable); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + public void testInit() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, defaultCallback); + psmp.shutdown(); + } + + private Playable writeTestPlayable(String downloadUrl, String fileUrl) { + final Context c = getInstrumentation().getTargetContext(); + Feed f = new Feed(0, new Date(), "f", "l", "d", null, null, null, null, "i", null, null, "l", false); + f.setItems(new ArrayList<FeedItem>()); + FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), false, f); + f.getItems().add(i); + FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0); + i.setMedia(media); + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + assertTrue(media.getId() != 0); + adapter.close(); + return media; + } + + + public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, false); + + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } finally { + countDownLatch.countDown(); + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + + private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + + private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (stream && reinit) ? 2 : 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (initialState != PlayerStatus.PLAYING) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + switch (newInfo.playerStatus) { + case PAUSED: + if (latchCount == countDownLatch.getCount()) + countDownLatch.countDown(); + else { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + case INITIALIZED: + if (stream && reinit && countDownLatch.getCount() < latchCount) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + } + } + + } + + @Override + public void shouldStop() { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to shouldStop"); + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.PLAYING) { + psmp.playMediaObject(p, stream, true, true); + } + psmp.pause(abandonAudioFocus, reinit); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || initialState != PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPauseDefaultState() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1); + } + + public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS); + } + + private void resumeTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (initialState == PlayerStatus.PAUSED || initialState == PlayerStatus.PLAYING) ? 2 : + (initialState == PlayerStatus.PREPARED) ? 1 : 0; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (newInfo.playerStatus == PlayerStatus.PLAYING) { + if (countDownLatch.getCount() == 0) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) { + assertionError = new AssertionFailedError("Unexpected call of onMediaPlayerError"); + } + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) { + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true); + } + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.resume(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || (initialState != PlayerStatus.PAUSED && initialState != PlayerStatus.PREPARED)); + psmp.shutdown(); + } + + public void testResumePausedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePreparedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePlayingState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PLAYING, 1); + } + + private void prepareTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (initialState == PlayerStatus.INITIALIZED && newInfo.playerStatus == PlayerStatus.PREPARED) { + countDownLatch.countDown(); + } else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.playerStatus) { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.INITIALIZED + || initialState == PlayerStatus.PLAYING + || initialState == PlayerStatus.PREPARED + || initialState == PlayerStatus.PAUSED) { + boolean prepareImmediately = (initialState != PlayerStatus.INITIALIZED); + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(p, false, startWhenPrepared, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.prepare(); + } + + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (initialState != PlayerStatus.INITIALIZED) { + assertEquals(initialState, psmp.getPSMPInfo().playerStatus); + } + + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testPrepareInitializedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparePlayingState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PLAYING, 1); + } + + public void testPreparePausedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PAUSED, 1); + } + + public void testPreparePreparedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PREPARED, 1); + } + + private void reinitTestSkeleton(final PlayerStatus initialState, final long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 2; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (newInfo.playerStatus == initialState) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount && newInfo.playerStatus == PlayerStatus.INITIALIZED) { + countDownLatch.countDown(); + } + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED; + boolean startImmediately = initialState != PlayerStatus.PREPARED; + psmp.playMediaObject(p, false, startImmediately, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.reinit(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testReinitPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitPausedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparedPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitInitializedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + private static class UnexpectedStateChange extends AssertionFailedError { + public UnexpectedStateChange(PlayerStatus status) { + super("Unexpected state change: " + status); + } + } +} 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 new file mode 100644 index 000000000..81d684595 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -0,0 +1,333 @@ +package de.test.antennapod.service.playback; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.playback.Playable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceTaskManager + */ +public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase { + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext())); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + public void testInit() { + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(getInstrumentation().getTargetContext(), defaultPSTM); + pstm.shutdown(); + } + + private List<FeedItem> writeTestQueue(String pref) { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + Feed f = new Feed(0, new Date(), "title", "link", "d", null, null, null, null, "id", null, "null", "url", false); + f.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), true, f)); + } + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + adapter.setQueue(f.getItems()); + adapter.close(); + + for (FeedItem item : f.getItems()) { + assertTrue(item.getId() != 0); + } + return f.getItems(); + } + + public void testGetQueueWriteBeforeCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + List<FeedItem> queue = writeTestQueue("a"); + assertNotNull(queue); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List<FeedItem> testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testGetQueueWriteAfterCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List<FeedItem> testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(testQueue.isEmpty()); + + + final CountDownLatch countDownLatch = new CountDownLatch(1); + EventDistributor.EventListener queueListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + countDownLatch.countDown(); + } + }; + EventDistributor.getInstance().register(queueListener); + List<FeedItem> queue = writeTestQueue("a"); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + countDownLatch.await(5000, TimeUnit.MILLISECONDS); + + assertNotNull(queue); + testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testStartPositionSaver() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + countDownLatch.countDown(); + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startPositionSaver(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsPositionSaverActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + assertTrue(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testCancelPositionSaver() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + pstm.cancelPositionSaver(); + assertFalse(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testStartWidgetUpdater() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.WIDGET_UPDATER_NOTIFICATION_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + countDownLatch.countDown(); + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startWidgetUpdater(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsWidgetUpdaterActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + assertTrue(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelWidgetUpdater() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.cancelWidgetUpdater(); + assertFalse(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksNoTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksAllTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.startPositionSaver(); + pstm.setSleepTimer(100000); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testSetSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 2000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + if (countDownLatch.getCount() == 0) { + fail(); + } + countDownLatch.countDown(); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testDisableSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 1000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + fail("Sleeptimer expired"); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + pstm.disableSleepTimer(); + assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)); + pstm.shutdown(); + } + + public void testIsSleepTimerActivePositive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + assertTrue(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testIsSleepTimerActiveNegative() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + pstm.disableSleepTimer(); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + private final PlaybackServiceTaskManager.PSTMCallback defaultPSTM = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }; +} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java new file mode 100644 index 000000000..63286d11d --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java @@ -0,0 +1,407 @@ +package de.test.antennapod.storage; + +import android.content.Context; +import android.test.InstrumentationTestCase; +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.FeedItemStatistics; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; + +/** + * Test class for DBReader + */ +public class DBReaderTest extends InstrumentationTestCase { + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + final Context context = getInstrumentation().getTargetContext(); + assertTrue(PodDBAdapter.deleteDatabase(context)); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); + feed.setItems(new ArrayList<FeedItem>()); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + List<Feed> expiredFeeds = DBReader.getExpiredFeedsList(context, expirationTime); + assertNotNull(expiredFeeds); + if (shouldReturn) { + assertTrue(expiredFeeds.size() == 1); + assertTrue(expiredFeeds.get(0).getId() == feed.getId()); + } else { + assertTrue(expiredFeeds.isEmpty()); + } + } + + public void testGetExpiredFeedsListShouldReturnFeed() { + final long expirationTime = 1000 * 60 * 60; // 1 hour + expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime - 1, expirationTime, true); + } + + public void testGetExpiredFeedsListShouldNotReturnFeed() { + final long expirationTime = 1000 * 60 * 60; // 1 hour + expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime / 2, expirationTime, false); + } + + + public void testGetFeedList() { + final Context context = getInstrumentation().getTargetContext(); + List<Feed> feeds = saveFeedlist(context, 10, 0, false); + List<Feed> savedFeeds = DBReader.getFeedList(context); + assertNotNull(savedFeeds); + assertEquals(feeds.size(), savedFeeds.size()); + for (int i = 0; i < feeds.size(); i++) { + assertTrue(savedFeeds.get(i).getId() == feeds.get(i).getId()); + } + } + + public void testGetFeedListSortOrder() { + final Context context = getInstrumentation().getTargetContext(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Feed feed1 = new Feed(0, new Date(), "A", "link", "d", null, null, null, "rss", "A", null, "", "", true); + Feed feed2 = new Feed(0, new Date(), "b", "link", "d", null, null, null, "rss", "b", null, "", "", true); + Feed feed3 = new Feed(0, new Date(), "C", "link", "d", null, null, null, "rss", "C", null, "", "", true); + Feed feed4 = new Feed(0, new Date(), "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(context); + 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()); + } + + public void testFeedListDownloadUrls() { + final Context context = getInstrumentation().getTargetContext(); + List<Feed> feeds = saveFeedlist(context, 10, 0, false); + List<String> urls = DBReader.getFeedListDownloadUrls(context); + assertNotNull(urls); + assertTrue(urls.size() == feeds.size()); + for (int i = 0; i < urls.size(); i++) { + assertEquals(urls.get(i), feeds.get(i).getDownload_url()); + } + } + + public void testLoadFeedDataOfFeedItemlist() { + final Context context = getInstrumentation().getTargetContext(); + final int numFeeds = 10; + final int numItems = 1; + List<Feed> feeds = saveFeedlist(context, numFeeds, numItems, false); + List<FeedItem> items = new ArrayList<FeedItem>(); + for (Feed f : feeds) { + for (FeedItem item : f.getItems()) { + item.setFeed(null); + item.setFeedId(f.getId()); + items.add(item); + } + } + DBReader.loadFeedDataOfFeedItemlist(context, 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()); + assertTrue(item.getFeed().getId() == feeds.get(i).getId()); + assertTrue(item.getFeedId() == item.getFeed().getId()); + } + } + } + + public void testGetFeedItemList() { + final Context context = getInstrumentation().getTargetContext(); + final int numFeeds = 1; + final int numItems = 10; + Feed feed = saveFeedlist(context, numFeeds, numItems, false).get(0); + List<FeedItem> items = feed.getItems(); + feed.setItems(null); + List<FeedItem> savedItems = DBReader.getFeedItemList(context, feed); + assertNotNull(savedItems); + assertTrue(savedItems.size() == items.size()); + for (int i = 0; i < savedItems.size(); i++) { + assertTrue(items.get(i).getId() == savedItems.get(i).getId()); + } + } + + private List<FeedItem> saveQueue(int numItems) { + if (numItems <= 0) { + throw new IllegalArgumentException("numItems<=0"); + } + final Context context = getInstrumentation().getTargetContext(); + List<Feed> feeds = saveFeedlist(context, numItems, numItems, false); + List<FeedItem> allItems = new ArrayList<FeedItem>(); + for (Feed f : feeds) { + allItems.addAll(f.getItems()); + } + // take random items from every feed + Random random = new Random(); + List<FeedItem> queue = new ArrayList<FeedItem>(); + while (queue.size() < numItems) { + int index = random.nextInt(numItems); + if (!queue.contains(allItems.get(index))) { + queue.add(allItems.get(index)); + } + } + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setQueue(queue); + adapter.close(); + return queue; + } + + public void testGetQueueIDList() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + List<FeedItem> queue = saveQueue(numItems); + List<Long> ids = DBReader.getQueueIDList(context); + assertNotNull(ids); + assertTrue(queue.size() == ids.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(ids.get(i) != 0); + assertTrue(queue.get(i).getId() == ids.get(i)); + } + } + + public void testGetQueue() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + List<FeedItem> queue = saveQueue(numItems); + List<FeedItem> savedQueue = DBReader.getQueue(context); + assertNotNull(savedQueue); + assertTrue(queue.size() == savedQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(savedQueue.get(i).getId() != 0); + assertTrue(queue.get(i).getId() == savedQueue.get(i).getId()); + } + } + + private List<FeedItem> saveDownloadedItems(int numItems) { + if (numItems <= 0) { + throw new IllegalArgumentException("numItems<=0"); + } + final Context context = getInstrumentation().getTargetContext(); + List<Feed> feeds = saveFeedlist(context, numItems, numItems, true); + List<FeedItem> items = new ArrayList<FeedItem>(); + for (Feed f : feeds) { + items.addAll(f.getItems()); + } + List<FeedItem> downloaded = new ArrayList<FeedItem>(); + 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 = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemlist(downloaded); + adapter.close(); + return downloaded; + } + + public void testGetDownloadedItems() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + List<FeedItem> downloaded = saveDownloadedItems(numItems); + List<FeedItem> downloaded_saved = DBReader.getDownloadedItems(context); + assertNotNull(downloaded_saved); + assertTrue(downloaded_saved.size() == downloaded.size()); + for (FeedItem item : downloaded_saved) { + assertNotNull(item.getMedia()); + assertTrue(item.getMedia().isDownloaded()); + assertNotNull(item.getMedia().getDownload_url()); + } + } + + private List<FeedItem> saveUnreadItems(int numItems) { + if (numItems <= 0) { + throw new IllegalArgumentException("numItems<=0"); + } + final Context context = getInstrumentation().getTargetContext(); + List<Feed> feeds = saveFeedlist(context, numItems, numItems, true); + List<FeedItem> items = new ArrayList<FeedItem>(); + for (Feed f : feeds) { + items.addAll(f.getItems()); + } + List<FeedItem> unread = new ArrayList<FeedItem>(); + Random random = new Random(); + + while (unread.size() < numItems) { + int i = random.nextInt(numItems); + if (!unread.contains(items.get(i))) { + FeedItem item = items.get(i); + item.setRead(false); + unread.add(item); + } + } + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemlist(unread); + adapter.close(); + return unread; + } + + public void testGetUnreadItemsList() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + + List<FeedItem> unread = saveUnreadItems(numItems); + List<FeedItem> unreadSaved = DBReader.getUnreadItemsList(context); + assertNotNull(unreadSaved); + assertTrue(unread.size() == unreadSaved.size()); + for (FeedItem item : unreadSaved) { + assertFalse(item.isRead()); + } + } + + public void testGetUnreadItemIds() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + + List<FeedItem> unread = saveUnreadItems(numItems); + long[] unreadIds = new long[unread.size()]; + for (int i = 0; i < unread.size(); i++) { + unreadIds[i] = unread.get(i).getId(); + } + long[] unreadSaved = DBReader.getUnreadItemIds(context); + assertNotNull(unreadSaved); + assertTrue(unread.size() == unreadSaved.length); + for (long savedId : unreadSaved) { + boolean found = false; + for (long id : unreadIds) { + if (id == savedId) { + found = true; + break; + } + } + assertTrue(found); + } + } + + public void testGetPlaybackHistory() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + final int playedItems = 5; + final int numFeeds = 1; + + Feed feed = DBTestUtils.saveFeedlist(context, numFeeds, numItems, true).get(0); + long[] ids = new long[playedItems]; + + PodDBAdapter adapter = new PodDBAdapter(context); + 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(context); + assertNotNull(saved); + assertEquals("Wrong size: ", playedItems, saved.size()); + for (int i = 0; i < playedItems; i++) { + FeedItem item = saved.get(i); + assertNotNull(item.getMedia().getPlaybackCompletionDate()); + assertEquals("Wrong sort order: ", item.getId(), ids[i]); + } + } + + public void testGetFeedStatisticsCheckOrder() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_FEEDS = 10; + final int NUM_ITEMS = 10; + List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, false); + List<FeedItemStatistics> statistics = DBReader.getFeedStatisticsList(context); + assertNotNull(statistics); + assertEquals(feeds.size(), statistics.size()); + for (int i = 0; i < NUM_FEEDS; i++) { + assertEquals("Wrong entry at index " + i, feeds.get(i).getId(), statistics.get(i).getFeedID()); + } + } + + public void testGetNavDrawerDataQueueEmptyNoUnreadItems() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_FEEDS = 10; + final int NUM_ITEMS = 10; + List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, true); + DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(context); + assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); + assertEquals(0, navDrawerData.numUnreadItems); + assertEquals(0, navDrawerData.queueSize); + } + + public void testGetNavDrawerDataQueueNotEmptyWithUnreadItems() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_FEEDS = 10; + final int NUM_ITEMS = 10; + final int NUM_QUEUE = 1; + final int NUM_UNREAD = 2; + List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, true); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (int i = 0; i < NUM_UNREAD; i++) { + FeedItem item = feeds.get(0).getItems().get(i); + item.setRead(false); + adapter.setSingleFeedItem(item); + } + List<FeedItem> queue = new ArrayList<FeedItem>(); + 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(context); + assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); + assertEquals(NUM_UNREAD, navDrawerData.numUnreadItems); + assertEquals(NUM_QUEUE, navDrawerData.queueSize); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java new file mode 100644 index 000000000..fd5b1c393 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -0,0 +1,326 @@ +package de.test.antennapod.storage; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.test.InstrumentationTestCase; +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.PodDBAdapter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static de.test.antennapod.storage.DBTestUtils.*; + +/** + * Test class for DBTasks + */ +public class DBTasksTest extends InstrumentationTestCase { + private static final String TEST_FOLDER = "testDBTasks"; + private static final int EPISODE_CACHE_SIZE = 5; + + private File destFolder; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + final Context context = getInstrumentation().getTargetContext(); + assertTrue(PodDBAdapter.deleteDatabase(context)); + + for (File f : destFolder.listFiles()) { + assertTrue(f.delete()); + } + assertTrue(destFolder.delete()); + + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + assertTrue(destFolder.exists()); + assertTrue(destFolder.canWrite()); + + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext().getApplicationContext()).edit(); + prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE)); + prefEdit.commit(); + } + + public void testPerformAutoCleanupShouldDelete() throws IOException { + final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + List<File> files = new ArrayList<File>(); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, 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)); + items.add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + 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(getInstrumentation().getTargetContext()); + for (int i = 0; i < files.size(); i++) { + if (i < EPISODE_CACHE_SIZE) { + assertTrue(files.get(i).exists()); + } else { + assertFalse(files.get(i).exists()); + } + } + } + + public void testPerformAutoCleanupShouldNotDeleteBecauseUnread() throws IOException { + final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + List<File> files = new ArrayList<File>(); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), false, feed); + + File f = new File(destFolder, "file " + i); + assertTrue(f.createNewFile()); + assertTrue(f.exists()); + files.add(f); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); + items.add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + 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(getInstrumentation().getTargetContext()); + for (File file : files) { + assertTrue(file.exists()); + } + } + + public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException { + final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + List<File> files = new ArrayList<File>(); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); + + File f = new File(destFolder, "file " + i); + assertTrue(f.createNewFile()); + assertTrue(f.exists()); + files.add(f); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); + items.add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.setQueue(items); + adapter.close(); + + assertTrue(feed.getId() != 0); + for (FeedItem item : items) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + DBTasks.performAutoCleanup(getInstrumentation().getTargetContext()); + 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 + */ + public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException { + final Context context = getInstrumentation().getTargetContext(); + // add feed with no enclosures so that item ID != media ID + saveFeedlist(context, 1, 10, false); + + // add candidate for performAutoCleanup + List<Feed> feeds = saveFeedlist(getInstrumentation().getTargetContext(), 1, 1, true); + FeedMedia m = feeds.get(0).getItems().get(0).getMedia(); + m.setDownloaded(true); + m.setFile_url("file"); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(m); + adapter.close(); + + testPerformAutoCleanupShouldNotDeleteBecauseInQueue(); + } + + public void testUpdateFeedNewFeed() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(), false, feed)); + } + Feed newFeed = DBTasks.updateFeed(context, feed)[0]; + + assertTrue(newFeed == feed); + assertTrue(feed.getId() != 0); + for (FeedItem item : feed.getItems()) { + assertFalse(item.isRead()); + assertTrue(item.getId() != 0); + } + } + + /** Two feeds with the same title, but different download URLs should be treated as different feeds. */ + public void testUpdateFeedSameTitle() { + final Context context = getInstrumentation().getTargetContext(); + + Feed feed1 = new Feed("url1", new Date(), "title"); + Feed feed2 = new Feed("url2", new Date(), "title"); + + feed1.setItems(new ArrayList<FeedItem>()); + feed2.setItems(new ArrayList<FeedItem>()); + + Feed savedFeed1 = DBTasks.updateFeed(context, feed1)[0]; + Feed savedFeed2 = DBTasks.updateFeed(context, feed2)[0]; + + assertTrue(savedFeed1.getId() != savedFeed2.getId()); + } + + public void testUpdateFeedUpdatedFeed() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS_OLD = 10; + final int NUM_ITEMS_NEW = 10; + + final Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS_OLD; i++) { + feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), true, feed)); + } + PodDBAdapter adapter = new PodDBAdapter(context); + 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<Long>(); + 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), true, feed)); + } + + final Feed newFeed = DBTasks.updateFeed(context, feed)[0]; + assertTrue(feed != newFeed); + + updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); + + final Feed feedFromDB = DBReader.getFeed(context, newFeed.getId()); + assertNotNull(feedFromDB); + assertTrue(feedFromDB.getId() == newFeed.getId()); + updatedFeedTest(feedFromDB, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); + } + + private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) { + assertTrue(newFeed.getId() == feedID); + assertTrue(newFeed.getItems().size() == NUM_ITEMS_NEW + NUM_ITEMS_OLD); + Collections.reverse(newFeed.getItems()); + Date lastDate = new Date(0); + for (int i = 0; i < NUM_ITEMS_OLD; i++) { + FeedItem item = newFeed.getItems().get(i); + assertTrue(item.getFeed() == newFeed); + assertTrue(item.getId() == itemIDs.get(i)); + assertTrue(item.isRead()); + 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); + assertTrue(item.getFeed() == newFeed); + assertTrue(item.getId() != 0); + assertFalse(item.isRead()); + assertTrue(item.getPubDate().getTime() >= lastDate.getTime()); + lastDate = item.getPubDate(); + } + } + + private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { + final Context context = getInstrumentation().getTargetContext(); + UserPreferences.setUpdateInterval(context, expirationTime); + Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); + feed.setItems(new ArrayList<FeedItem>()); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + List<Feed> expiredFeeds = DBTasks.getExpiredFeeds(context); + assertNotNull(expiredFeeds); + if (shouldReturn) { + assertTrue(expiredFeeds.size() == 1); + assertTrue(expiredFeeds.get(0).getId() == feed.getId()); + } else { + assertTrue(expiredFeeds.isEmpty()); + } + } + + public void testGetExpiredFeedsTestShouldReturn() { + final long expirationTime = 1000 * 60 * 60; + expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime - 1, expirationTime, true); + } + + public void testGetExpiredFeedsTestShouldNotReturn() { + final long expirationTime = 1000 * 60 * 60; + expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime / 2, expirationTime, false); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java new file mode 100644 index 000000000..e7d6396f5 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java @@ -0,0 +1,57 @@ +package de.test.antennapod.storage; + +import android.content.Context; +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.PodDBAdapter; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import junit.framework.Assert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Utility methods for DB* tests. + */ +public class DBTestUtils { + + public static List<Feed> saveFeedlist(Context context, int numFeeds, int numItems, boolean withMedia) { + if (numFeeds <= 0) { + throw new IllegalArgumentException("numFeeds<=0"); + } + if (numItems < 0) { + throw new IllegalArgumentException("numItems<0"); + } + + List<Feed> feeds = new ArrayList<Feed>(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (int i = 0; i < numFeeds; i++) { + Feed f = new Feed(0, new Date(), "feed " + i, "link" + i, "descr", null, null, + null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus()); + f.setItems(new ArrayList<FeedItem>()); + for (int j = 0; j < numItems; j++) { + FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), + true, f); + if (withMedia) { + FeedMedia media = new FeedMedia(item, "url" + j, 1, "audio/mp3"); + item.setMedia(media); + } + f.getItems().add(item); + } + Collections.sort(f.getItems(), new FeedItemPubdateComparator()); + adapter.setCompleteFeed(f); + Assert.assertTrue(f.getId() != 0); + for (FeedItem item : f.getItems()) { + Assert.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 new file mode 100644 index 000000000..4678a843b --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java @@ -0,0 +1,796 @@ +package de.test.antennapod.storage; + +import android.content.Context; +import android.database.Cursor; +import android.test.InstrumentationTestCase; +import android.util.Log; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +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.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Test class for DBWriter + */ +public class DBWriterTest extends InstrumentationTestCase { + private static final String TAG = "DBWriterTest"; + private static final String TEST_FOLDER = "testDBWriter"; + private static final long TIMEOUT = 5L; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + final Context context = getInstrumentation().getTargetContext(); + assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext())); + + File testDir = context.getExternalFilesDir(TEST_FOLDER); + assertNotNull(testDir); + for (File f : testDir.listFiles()) { + f.delete(); + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + public void testDeleteFeedMediaOfItemFileExists() throws IOException, ExecutionException, InterruptedException { + File dest = new File(getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); + + assertTrue(dest.createNewFile()); + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), true, feed); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0); + item.setMedia(media); + + items.add(item); + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + assertTrue(media.getId() != 0); + assertTrue(item.getId() != 0); + + DBWriter.deleteFeedMediaOfItem(getInstrumentation().getTargetContext(), media.getId()).get(); + media = DBReader.getFeedMedia(getInstrumentation().getTargetContext(), media.getId()); + assertNotNull(media); + assertFalse(dest.exists()); + assertFalse(media.isDownloaded()); + assertNull(media.getFile_url()); + } + + public void testDeleteFeed() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // 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(), true, 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); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + for (File f : itemFiles) { + assertFalse(f.exists()); + } + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedNoImage() throws ExecutionException, InterruptedException, IOException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + feed.setImage(null); + + List<File> itemFiles = new ArrayList<File>(); + // 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(), true, 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); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + 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(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + // check if files still exist + for (File f : itemFiles) { + assertFalse(f.exists()); + } + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedNoItems() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(null); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + + public void testDeleteFeedNoFeedMedia() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + // create items + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed); + feed.getItems().add(item); + + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedWithItemImages() throws InterruptedException, ExecutionException, TimeoutException, IOException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + // create items with images + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed); + feed.getItems().add(item); + File itemImageFile = new File(destFolder, "item-image-" + i); + FeedImage itemImage = new FeedImage(0, "item-image" + i, itemImageFile.getAbsolutePath(), "url", true); + item.setImage(itemImage); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getImage().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(item.getImage().getId()); + assertEquals(0, c.getCount()); + c.close(); + } + } + + public void testDeleteFeedWithQueueItems() throws ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // 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(), true, feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + + List<FeedItem> queue = new ArrayList<FeedItem>(); + queue.addAll(feed.getItems()); + adapter.open(); + adapter.setQueue(queue); + + Cursor queueCursor = adapter.getQueueIDCursor(); + assertTrue(queueCursor.getCount() == queue.size()); + queueCursor.close(); + + adapter.close(); + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + adapter.open(); + + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + c = adapter.getQueueCursor(); + assertTrue(c.getCount() == 0); + c.close(); + adapter.close(); + } + + public void testDeleteFeedNoDownloadedFiles() throws ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setOwner(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // 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(), true, feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + private FeedMedia playbackHistorySetup(Date playbackCompletionDate) { + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); + FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate, 0); + feed.getItems().add(item); + item.setMedia(media); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + assertTrue(media.getId() != 0); + return media; + } + + public void testAddItemToPlaybackHistoryNotPlayedYet() throws ExecutionException, InterruptedException { + final Context context = getInstrumentation().getTargetContext(); + + FeedMedia media = playbackHistorySetup(null); + DBWriter.addItemToPlaybackHistory(context, media).get(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + media = DBReader.getFeedMedia(context, media.getId()); + adapter.close(); + + assertNotNull(media); + assertNotNull(media.getPlaybackCompletionDate()); + } + + public void testAddItemToPlaybackHistoryAlreadyPlayed() throws ExecutionException, InterruptedException { + final long OLD_DATE = 0; + final Context context = getInstrumentation().getTargetContext(); + + FeedMedia media = playbackHistorySetup(new Date(OLD_DATE)); + DBWriter.addItemToPlaybackHistory(getInstrumentation().getTargetContext(), media).get(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + media = DBReader.getFeedMedia(context, media.getId()); + adapter.close(); + + assertNotNull(media); + assertNotNull(media.getPlaybackCompletionDate()); + assertFalse(OLD_DATE == media.getPlaybackCompletionDate().getTime()); + } + + private Feed queueTestSetupMultipleItems(final int NUM_ITEMS) throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed); + feed.getItems().add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + List<Future<?>> futures = new ArrayList<Future<?>>(); + for (FeedItem item : feed.getItems()) { + futures.add(DBWriter.addQueueItem(context, item.getId())); + } + for (Future<?> f : futures) { + f.get(TIMEOUT, TimeUnit.SECONDS); + } + return feed; + } + + public void testAddQueueItemSingleItem() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); + feed.getItems().add(item); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(item.getId() != 0); + DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getQueueIDCursor(); + assertTrue(cursor.moveToFirst()); + assertTrue(cursor.getLong(0) == item.getId()); + cursor.close(); + adapter.close(); + } + + public void testAddQueueItemSingleItemAlreadyInQueue() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); + feed.getItems().add(item); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(item.getId() != 0); + DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS); + + adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getQueueIDCursor(); + assertTrue(cursor.moveToFirst()); + assertTrue(cursor.getLong(0) == item.getId()); + cursor.close(); + adapter.close(); + + DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS); + adapter = new PodDBAdapter(context); + adapter.open(); + cursor = adapter.getQueueIDCursor(); + assertTrue(cursor.moveToFirst()); + assertTrue(cursor.getLong(0) == item.getId()); + assertTrue(cursor.getCount() == 1); + cursor.close(); + adapter.close(); + } + + public void testAddQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + + Feed feed = queueTestSetupMultipleItems(NUM_ITEMS); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getQueueIDCursor(); + assertTrue(cursor.moveToFirst()); + assertTrue(cursor.getCount() == NUM_ITEMS); + for (int i = 0; i < NUM_ITEMS; i++) { + assertTrue(cursor.moveToPosition(i)); + assertTrue(cursor.getLong(0) == feed.getItems().get(i).getId()); + } + cursor.close(); + adapter.close(); + } + + public void testClearQueue() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + + Feed feed = queueTestSetupMultipleItems(NUM_ITEMS); + DBWriter.clearQueue(context).get(TIMEOUT, TimeUnit.SECONDS); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getQueueIDCursor(); + assertFalse(cursor.moveToFirst()); + cursor.close(); + adapter.close(); + } + + public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { + final int NUM_ITEMS = 10; + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed); + feed.getItems().add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) { + final long id = feed.getItems().get(removeIndex).getId(); + adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setQueue(feed.getItems()); + adapter.close(); + + DBWriter.removeQueueItem(context, id, false).get(TIMEOUT, TimeUnit.SECONDS); + adapter = new PodDBAdapter(context); + adapter.open(); + Cursor queue = adapter.getQueueIDCursor(); + assertTrue(queue.getCount() == NUM_ITEMS - 1); + for (int i = 0; i < queue.getCount(); i++) { + assertTrue(queue.moveToPosition(i)); + final long queueID = queue.getLong(0); + assertTrue(queueID != id); // removed item is no longer in queue + boolean idFound = false; + for (FeedItem item : feed.getItems()) { // items that were not removed are still in the queue + idFound = idFound | (item.getId() == queueID); + } + assertTrue(idFound); + } + + queue.close(); + adapter.close(); + } + } + + public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { + final int NUM_ITEMS = 10; + final Context context = getInstrumentation().getTargetContext(); + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed); + feed.getItems().add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(context); + 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("testMoveQueueItem: From=%d, To=%d", from, to)); + final long fromID = feed.getItems().get(from).getId(); + + adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setQueue(feed.getItems()); + adapter.close(); + + DBWriter.moveQueueItem(context, from, to, false).get(TIMEOUT, TimeUnit.SECONDS); + adapter = new PodDBAdapter(context); + adapter.open(); + Cursor queue = adapter.getQueueIDCursor(); + assertTrue(queue.getCount() == NUM_ITEMS); + assertTrue(queue.moveToPosition(from)); + assertFalse(queue.getLong(0) == fromID); + assertTrue(queue.moveToPosition(to)); + assertTrue(queue.getLong(0) == fromID); + + queue.close(); + adapter.close(); + } + } + } + + public void testMarkFeedRead() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), false, feed); + feed.getItems().add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + + DBWriter.markFeedRead(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + List<FeedItem> loadedItems = DBReader.getFeedItemList(context, feed); + for (FeedItem item : loadedItems) { + assertTrue(item.isRead()); + } + } + + public void testMarkAllItemsReadSameFeed() throws InterruptedException, ExecutionException, TimeoutException { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), false, feed); + feed.getItems().add(item); + } + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + + DBWriter.markAllItemsRead(context).get(TIMEOUT, TimeUnit.SECONDS); + List<FeedItem> loadedItems = DBReader.getFeedItemList(context, feed); + for (FeedItem item : loadedItems) { + assertTrue(item.isRead()); + } + } + +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java new file mode 100644 index 000000000..3656582e1 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -0,0 +1,115 @@ +package de.test.antennapod.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.test.ActivityInstrumentationTestCase2; +import android.view.View; +import com.robotium.solo.Solo; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +/** + * User interface tests for MainActivity + */ +public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> { + + private Solo solo; + private UITestUtils uiTestUtils; + + public MainActivityTest() { + super(MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + solo = new Solo(getInstrumentation(), getActivity()); + uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext()); + uiTestUtils.setup(); + // create database + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.close(); + + // override first launch preference + SharedPreferences prefs = getInstrumentation().getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false).commit(); + } + + @Override + protected void tearDown() throws Exception { + uiTestUtils.tearDown(); + solo.finishOpenedActivities(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + super.tearDown(); + } + + public void testAddFeed() throws Exception { + uiTestUtils.addHostedFeedData(); + final Feed feed = uiTestUtils.hostedFeeds.get(0); + solo.setNavigationDrawer(Solo.OPENED); + solo.clickOnText(solo.getString(R.string.add_feed_label)); + solo.enterText(0, feed.getDownload_url()); + solo.clickOnButton(0); + solo.waitForActivity(DefaultOnlineFeedViewActivity.class); + solo.waitForView(R.id.butSubscribe); + assertEquals(solo.getString(R.string.subscribe_label), solo.getButton(0).getText().toString()); + solo.clickOnButton(0); + solo.waitForText(solo.getString(R.string.subscribed_label)); + } + + public void testClickNavDrawer() throws Exception { + uiTestUtils.addLocalFeedData(false); + final View home = solo.getView(UITestUtils.HOME_VIEW); + + // all episodes + solo.waitForView(android.R.id.list); + assertEquals(solo.getString(R.string.all_episodes_label), getActionbarTitle()); + // queue + solo.clickOnView(home); + solo.clickOnText(solo.getString(R.string.queue_label)); + solo.waitForView(android.R.id.list); + assertEquals(solo.getString(R.string.queue_label), getActionbarTitle()); + + // downloads + solo.clickOnView(home); + solo.clickOnText(solo.getString(R.string.downloads_label)); + solo.waitForView(android.R.id.list); + assertEquals(solo.getString(R.string.downloads_label), getActionbarTitle()); + + // playback history + solo.clickOnView(home); + solo.clickOnText(solo.getString(R.string.playback_history_label)); + solo.waitForView(android.R.id.list); + assertEquals(solo.getString(R.string.playback_history_label), getActionbarTitle()); + + // add podcast + solo.clickOnView(home); + solo.clickOnText(solo.getString(R.string.add_feed_label)); + solo.waitForView(R.id.txtvFeedurl); + assertEquals(solo.getString(R.string.add_feed_label), getActionbarTitle()); + + // podcasts + for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) { + Feed f = uiTestUtils.hostedFeeds.get(i); + solo.clickOnView(home); + solo.clickOnText(f.getTitle()); + solo.waitForView(android.R.id.list); + assertEquals("", getActionbarTitle()); + } + } + + private String getActionbarTitle() { + return ((MainActivity)solo.getCurrentActivity()).getMainActivtyActionBar().getTitle().toString(); + } + + public void testGoToPreferences() { + solo.setNavigationDrawer(Solo.CLOSED); + solo.clickOnMenuItem(solo.getString(R.string.settings_label)); + solo.waitForActivity(PreferenceActivity.class); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java new file mode 100644 index 000000000..daae4bd62 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java @@ -0,0 +1,149 @@ +package de.test.antennapod.ui; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.test.ActivityInstrumentationTestCase2; +import android.widget.TextView; +import com.robotium.solo.Solo; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import java.util.List; + +/** + * Test cases for starting and ending playback from the MainActivity and AudioPlayerActivity + */ +public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> { + + private Solo solo; + private UITestUtils uiTestUtils; + + public PlaybackTest() { + super(MainActivity.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + solo = new Solo(getInstrumentation(), getActivity()); + uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext()); + uiTestUtils.setup(); + // create database + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.close(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); + prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT, false).commit(); + } + + @Override + public void tearDown() throws Exception { + uiTestUtils.tearDown(); + solo.finishOpenedActivities(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + + // shut down playback service + skipEpisode(); + getInstrumentation().getTargetContext().sendBroadcast( + new Intent(PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + + super.tearDown(); + } + + private void setContinuousPlaybackPreference(boolean value) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); + prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit(); + } + + private void skipEpisode() { + Intent skipIntent = new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE); + getInstrumentation().getTargetContext().sendBroadcast(skipIntent); + } + + private void startLocalPlayback() { + assertTrue(solo.waitForActivity(MainActivity.class)); + solo.setNavigationDrawer(Solo.CLOSED); + solo.clickOnView(solo.getView(R.id.butSecondaryAction)); + assertTrue(solo.waitForActivity(AudioplayerActivity.class)); + assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); + } + + private void startLocalPlaybackFromQueue() { + assertTrue(solo.waitForActivity(MainActivity.class)); + solo.clickOnView(solo.getView(UITestUtils.HOME_VIEW)); + solo.clickOnText(solo.getString(R.string.queue_label)); + assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); + solo.clickOnImageButton(0); + assertTrue(solo.waitForActivity(AudioplayerActivity.class)); + assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); + } + + public void testStartLocal() throws Exception { + uiTestUtils.addLocalFeedData(true); + DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); + startLocalPlayback(); + + solo.clickOnView(solo.getView(R.id.butPlay)); + } + + public void testContinousPlaybackOffSingleEpisode() throws Exception { + setContinuousPlaybackPreference(false); + uiTestUtils.addLocalFeedData(true); + DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); + startLocalPlayback(); + assertTrue(solo.waitForActivity(MainActivity.class)); + } + + + public void testContinousPlaybackOffMultipleEpisodes() throws Exception { + setContinuousPlaybackPreference(false); + uiTestUtils.addLocalFeedData(true); + List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext()); + FeedItem second = queue.get(1); + + startLocalPlaybackFromQueue(); + assertTrue(solo.waitForText(second.getTitle())); + } + + public void testContinuousPlaybackOnMultipleEpisodes() throws Exception { + setContinuousPlaybackPreference(true); + uiTestUtils.addLocalFeedData(true); + List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext()); + FeedItem second = queue.get(1); + + startLocalPlaybackFromQueue(); + assertTrue(solo.waitForText(second.getTitle())); + } + + /** + * Check if an episode can be played twice without problems. + */ + private void replayEpisodeCheck(boolean followQueue) throws Exception { + setContinuousPlaybackPreference(followQueue); + uiTestUtils.addLocalFeedData(true); + DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); + String title = ((TextView) solo.getView(R.id.txtvTitle)).getText().toString(); + startLocalPlayback(); + assertTrue(solo.waitForText(title)); + assertTrue(solo.waitForActivity(MainActivity.class)); + startLocalPlayback(); + assertTrue(solo.waitForText(title)); + assertTrue(solo.waitForActivity(MainActivity.class)); + } + + public void testReplayEpisodeContinuousPlaybackOn() throws Exception { + replayEpisodeCheck(true); + } + + public void testReplayEpisodeContinuousPlaybackOff() throws Exception { + replayEpisodeCheck(false); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java new file mode 100644 index 000000000..55fffb80a --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -0,0 +1,200 @@ +package de.test.antennapod.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.test.antennapod.util.service.download.HTTPBin; +import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; +import junit.framework.Assert; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Utility methods for UI tests. + * Starts a web server that hosts feeds, episodes and images. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +public class UITestUtils { + + private static final String DATA_FOLDER = "test/UITestUtils"; + + public static final int NUM_FEEDS = 5; + public static final int NUM_ITEMS_PER_FEED = 10; + + public static final int HOME_VIEW = (Build.VERSION.SDK_INT >= 11) ? android.R.id.home : R.id.home; + + + private Context context; + private HTTPBin server = new HTTPBin(); + private File destDir; + private File hostedFeedDir; + private File hostedMediaDir; + + public List<Feed> hostedFeeds = new ArrayList<Feed>(); + + public UITestUtils(Context context) { + this.context = context; + } + + + public void setup() throws IOException { + destDir = context.getExternalFilesDir(DATA_FOLDER); + destDir.mkdir(); + hostedFeedDir = new File(destDir, "hostedFeeds"); + hostedFeedDir.mkdir(); + hostedMediaDir = new File(destDir, "hostedMediaDir"); + hostedMediaDir.mkdir(); + Assert.assertTrue(destDir.exists()); + Assert.assertTrue(hostedFeedDir.exists()); + Assert.assertTrue(hostedMediaDir.exists()); + server.start(); + } + + public void tearDown() throws IOException { + FileUtils.deleteDirectory(destDir); + FileUtils.deleteDirectory(hostedMediaDir); + FileUtils.deleteDirectory(hostedFeedDir); + server.stop(); + + if (localFeedDataAdded) { + PodDBAdapter.deleteDatabase(context); + } + } + + private String hostFeed(Feed feed) throws IOException { + File feedFile = new File(hostedFeedDir, feed.getTitle()); + FileOutputStream out = new FileOutputStream(feedFile); + RSS2Generator generator = new RSS2Generator(); + generator.writeFeed(feed, out, "UTF-8", 0); + out.close(); + int id = server.serveFile(feedFile); + Assert.assertTrue(id != -1); + return String.format("%s/files/%d", HTTPBin.BASE_URL, id); + } + + private String hostFile(File file) { + int id = server.serveFile(file); + Assert.assertTrue(id != -1); + return String.format("%s/files/%d", HTTPBin.BASE_URL, id); + } + + private File newBitmapFile(String name) throws IOException { + File imgFile = new File(destDir, name); + Bitmap bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888); + FileOutputStream out = new FileOutputStream(imgFile); + bitmap.compress(Bitmap.CompressFormat.PNG, 1, out); + out.close(); + return imgFile; + } + + private File newMediaFile(String name) throws IOException { + File mediaFile = new File(hostedMediaDir, name); + Assert.assertFalse(mediaFile.exists()); + + InputStream in = context.getAssets().open("testfile.mp3"); + Assert.assertNotNull(in); + + FileOutputStream out = new FileOutputStream(mediaFile); + IOUtils.copy(in, out); + out.close(); + + return mediaFile; + } + + private boolean feedDataHosted = false; + + /** + * Adds feeds, images and episodes to the webserver for testing purposes. + */ + public void addHostedFeedData() throws IOException { + if (feedDataHosted) throw new IllegalStateException("addHostedFeedData was called twice on the same instance"); + for (int i = 0; i < NUM_FEEDS; i++) { + File bitmapFile = newBitmapFile("image" + i); + FeedImage image = new FeedImage(0, "image " + i, null, hostFile(bitmapFile), false); + Feed feed = new Feed(0, new Date(), "Title " + i, "http://example.com/" + i, "Description of feed " + i, + "http://example.com/pay/feed" + i, "author " + i, "en", Feed.TYPE_RSS2, "feed" + i, image, null, + "http://example.com/feed/src/" + i, false); + image.setOwner(feed); + + // create items + List<FeedItem> items = new ArrayList<FeedItem>(); + for (int j = 0; j < NUM_ITEMS_PER_FEED; j++) { + FeedItem item = new FeedItem(0, "item" + j, "item" + j, "http://example.com/feed" + i + "/item/" + j, new Date(), true, feed); + items.add(item); + + File mediaFile = newMediaFile("feed-" + i + "-episode-" + j + ".mp3"); + item.setMedia(new FeedMedia(0, item, 0, 0, mediaFile.length(), "audio/mp3", null, hostFile(mediaFile), false, null, 0)); + + } + feed.setItems(items); + feed.setDownload_url(hostFeed(feed)); + hostedFeeds.add(feed); + } + feedDataHosted = true; + } + + + private boolean localFeedDataAdded = false; + + /** + * Adds feeds, images and episodes to the local database. This method will also call addHostedFeedData if it has not + * been called yet. + * + * Adds one item of each feed to the queue and to the playback history. + * + * This method should NOT be called if the testing class wants to download the hosted feed data. + * + * @param downloadEpisodes true if episodes should also be marked as downloaded. + */ + public void addLocalFeedData(boolean downloadEpisodes) throws Exception { + if (localFeedDataAdded) throw new IllegalStateException("addLocalFeedData was called twice on the same instance"); + if (!feedDataHosted) { + addHostedFeedData(); + } + + List<FeedItem> queue = new ArrayList<FeedItem>(); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (Feed feed : hostedFeeds) { + feed.setDownloaded(true); + if (feed.getImage() != null) { + FeedImage image = feed.getImage(); + image.setFile_url(image.getDownload_url()); + image.setDownloaded(true); + } + if (downloadEpisodes) { + for (FeedItem item : feed.getItems()) { + if (item.hasMedia()) { + FeedMedia media = item.getMedia(); + int fileId = Integer.parseInt(StringUtils.substringAfter(media.getDownload_url(), "files/")); + media.setFile_url(server.accessFile(fileId).getAbsolutePath()); + media.setDownloaded(true); + } + } + } + + queue.add(feed.getItems().get(0)); + feed.getItems().get(1).getMedia().setPlaybackCompletionDate(new Date()); + } + adapter.setCompleteFeed(hostedFeeds.toArray(new Feed[hostedFeeds.size()])); + adapter.setQueue(queue); + adapter.close(); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java new file mode 100644 index 000000000..6c5a350de --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java @@ -0,0 +1,94 @@ +package de.test.antennapod.ui; + +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import org.apache.http.HttpStatus; + +import java.io.File; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +/** + * Test for the UITestUtils. Makes sure that all URLs are reachable and that the class does not cause any crashes. + */ +public class UITestUtilsTest extends InstrumentationTestCase { + + private UITestUtils uiTestUtils; + + @Override + protected void setUp() throws Exception { + super.setUp(); + uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext()); + uiTestUtils.setup(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + uiTestUtils.tearDown(); + } + + public void testAddHostedFeeds() throws Exception { + uiTestUtils.addHostedFeedData(); + final List<Feed> feeds = uiTestUtils.hostedFeeds; + assertNotNull(feeds); + assertFalse(feeds.isEmpty()); + + for (Feed feed : feeds) { + testUrlReachable(feed.getDownload_url()); + if (feed.getImage() != null) { + testUrlReachable(feed.getImage().getDownload_url()); + } + for (FeedItem item : feed.getItems()) { + if (item.hasMedia()) { + testUrlReachable(item.getMedia().getDownload_url()); + } + } + } + } + + private void testUrlReachable(String strUtl) throws Exception { + URL url = new URL(strUtl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.connect(); + int rc = conn.getResponseCode(); + assertEquals(HttpStatus.SC_OK, rc); + conn.disconnect(); + } + + private void addLocalFeedDataCheck(boolean downloadEpisodes) throws Exception { + uiTestUtils.addLocalFeedData(downloadEpisodes); + assertNotNull(uiTestUtils.hostedFeeds); + assertFalse(uiTestUtils.hostedFeeds.isEmpty()); + + for (Feed feed : uiTestUtils.hostedFeeds) { + assertTrue(feed.getId() != 0); + if (feed.getImage() != null) { + assertTrue(feed.getImage().getId() != 0); + } + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + if (item.hasMedia()) { + assertTrue(item.getMedia().getId() != 0); + if (downloadEpisodes) { + assertTrue(item.getMedia().isDownloaded()); + assertNotNull(item.getMedia().getFile_url()); + File file = new File(item.getMedia().getFile_url()); + assertTrue(file.exists()); + } + } + } + } + } + + public void testAddLocalFeedDataNoDownload() throws Exception { + addLocalFeedDataCheck(false); + } + + public void testAddLocalFeedDataDownload() throws Exception { + addLocalFeedDataCheck(true); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java new file mode 100644 index 000000000..da6f07cab --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java @@ -0,0 +1,38 @@ +package de.test.antennapod.ui; + +import android.test.ActivityInstrumentationTestCase2; + +import com.robotium.solo.Solo; + +import de.danoeh.antennapod.activity.VideoplayerActivity; + +/** + * Test class for VideoplayerActivity + */ +public class VideoplayerActivityTest extends ActivityInstrumentationTestCase2<VideoplayerActivity> { + + private Solo solo; + + public VideoplayerActivityTest() { + super(VideoplayerActivity.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + solo = new Solo(getInstrumentation(), getActivity()); + } + + @Override + public void tearDown() throws Exception { + solo.finishOpenedActivities(); + super.tearDown(); + } + + /** + * Test if activity can be started. + */ + public void testStartActivity() throws Exception { + solo.waitForActivity(VideoplayerActivity.class); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java new file mode 100644 index 000000000..47fca41ba --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java @@ -0,0 +1,35 @@ +package de.test.antennapod.util; + +import android.test.AndroidTestCase; + +import de.danoeh.antennapod.core.util.Converter; + +/** + * Test class for converter + */ +public class ConverterTest extends AndroidTestCase { + + public void testGetDurationStringLong() throws Exception { + String expected = "13:05:10"; + int input = 47110000; + assertEquals(expected, Converter.getDurationStringLong(input)); + } + + public void testGetDurationStringShort() throws Exception { + String expected = "13:05"; + int input = 47110000; + assertEquals(expected, Converter.getDurationStringShort(input)); + } + + public void testDurationStringLongToMs() throws Exception { + String input = "01:20:30"; + long expected = 4830000; + assertEquals(expected, Converter.durationStringLongToMs(input)); + } + + public void testDurationStringShortToMs() throws Exception { + String input = "8:30"; + long expected = 30600000; + assertEquals(expected, Converter.durationStringShortToMs(input)); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java new file mode 100644 index 000000000..6d24fa526 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java @@ -0,0 +1,59 @@ +package de.test.antennapod.util; + +import java.io.File; +import java.io.IOException; + +import de.danoeh.antennapod.core.util.FileNameGenerator; +import android.test.AndroidTestCase; + +public class FilenameGeneratorTest extends AndroidTestCase { + + private static final String VALID1 = "abc abc"; + private static final String INVALID1 = "ab/c: <abc"; + private static final String INVALID2 = "abc abc "; + + public FilenameGeneratorTest() { + super(); + } + + public void testGenerateFileName() throws IOException { + String result = FileNameGenerator.generateFileName(VALID1); + assertEquals(result, VALID1); + createFiles(result); + } + + public void testGenerateFileName1() throws IOException { + String result = FileNameGenerator.generateFileName(INVALID1); + assertEquals(result, VALID1); + createFiles(result); + } + + public void testGenerateFileName2() throws IOException { + String result = FileNameGenerator.generateFileName(INVALID2); + assertEquals(result, VALID1); + createFiles(result); + } + + /** + * Tests if files can be created. + * + * @throws IOException + */ + private void createFiles(String name) throws IOException { + File cache = getContext().getExternalCacheDir(); + File testFile = new File(cache, name); + testFile.mkdir(); + assertTrue(testFile.exists()); + testFile.delete(); + assertTrue(testFile.createNewFile()); + + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + File f = new File(getContext().getExternalCacheDir(), VALID1); + f.delete(); + } + +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java new file mode 100644 index 000000000..7bdcfb898 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java @@ -0,0 +1,21 @@ +package de.test.antennapod.util; + +import android.test.AndroidTestCase; +import de.danoeh.antennapod.core.util.URIUtil; + +/** + * Test class for URIUtil + */ +public class URIUtilTest extends AndroidTestCase { + + public void testGetURIFromRequestUrlShouldNotEncode() { + final String testUrl = "http://example.com/this%20is%20encoded"; + assertEquals(testUrl, URIUtil.getURIFromRequestUrl(testUrl).toString()); + } + + public void testGetURIFromRequestUrlShouldEncode() { + final String testUrl = "http://example.com/this is not encoded"; + final String expected = "http://example.com/this%20is%20not%20encoded"; + assertEquals(expected, URIUtil.getURIFromRequestUrl(testUrl).toString()); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java new file mode 100644 index 000000000..47b58268b --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java @@ -0,0 +1,76 @@ +package de.test.antennapod.util; + +import android.test.AndroidTestCase; +import de.danoeh.antennapod.core.util.URLChecker; + +/** + * Test class for URLChecker + */ +public class URLCheckerTest extends AndroidTestCase { + + public void testCorrectURLHttp() { + final String in = "http://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals(in, out); + } + + public void testCorrectURLHttps() { + final String in = "https://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals(in, out); + } + + public void testMissingProtocol() { + final String in = "example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testFeedProtocol() { + final String in = "feed://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testPcastProtocolNoScheme() { + final String in = "pcast://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testItpcProtocol() { + final String in = "itpc://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testWhiteSpaceUrlShouldNotAppend() { + final String in = "\n http://example.com \t"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testWhiteSpaceShouldAppend() { + final String in = "\n example.com \t"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testAntennaPodSubscribeProtocolNoScheme() throws Exception { + final String in = "antennapod-subscribe://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("http://example.com", out); + } + + public void testPcastProtocolWithScheme() { + final String in = "pcast://https://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("https://example.com", out); + } + + public void testAntennaPodSubscribeProtocolWithScheme() throws Exception { + final String in = "antennapod-subscribe://https://example.com"; + final String out = URLChecker.prepareURL(in); + assertEquals("https://example.com", out); + } +} 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 new file mode 100644 index 000000000..2c56b71cc --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java @@ -0,0 +1,127 @@ +package de.test.antennapod.util.playback; + +import android.content.Context; +import android.test.InstrumentationTestCase; + +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; + +/** + * Test class for timeline + */ +public class TimelineTest extends InstrumentationTestCase { + + private Context context; + + @Override + public void setUp() throws Exception { + super.setUp(); + context = getInstrumentation().getTargetContext(); + } + + private Playable newTestPlayable(List<Chapter> chapters, String shownotes) { + FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), true, null); + item.setChapters(chapters); + item.setContentEncoded(shownotes); + FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3"); + media.setDuration(Integer.MAX_VALUE); + item.setMedia(media); + return media; + } + + public void testProcessShownotesAddTimecodeHHMMSSNoChapters() throws Exception { + 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>"); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeHHMMNoChapters() throws Exception { + 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>"); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeParentheses() throws Exception { + 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>"); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeBrackets() throws Exception { + 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>"); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeAngleBrackets() throws Exception { + 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>"); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + 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); + } + + public void testIsTimecodeLink() throws Exception { + 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")); + } + + public void testGetTimecodeLinkTime() throws Exception { + 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/service/download/HTTPBin.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java new file mode 100644 index 000000000..5cb723446 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java @@ -0,0 +1,346 @@ +package de.test.antennapod.util.service.download; + +import android.util.Base64; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.*; +import java.net.URLConnection; +import java.util.*; +import java.util.zip.GZIPOutputStream; + +/** + * Http server for testing purposes + * <p/> + * Supported features: + * <p/> + * /status/code: Returns HTTP response with the given status code + * /redirect/n: Redirects n times + * /delay/n: Delay response for n seconds + * /basic-auth/username/password: Basic auth with username and password + * /gzip/n: Send gzipped data of size n bytes + * /files/id: Accesses the file with the specified ID (this has to be added first via serveFile). + */ +public class HTTPBin extends NanoHTTPD { + private static final String TAG = "HTTPBin"; + public static final int PORT = 8124; + public static final String BASE_URL = "http://127.0.0.1:" + HTTPBin.PORT; + + + private static final String MIME_HTML = "text/html"; + private static final String MIME_PLAIN = "text/plain"; + + private List<File> servedFiles; + + public HTTPBin() { + super(PORT); + this.servedFiles = new ArrayList<File>(); + } + + /** + * Adds the given file to the server. + * + * @return The ID of the file or -1 if the file could not be added to the server. + */ + public synchronized int serveFile(File file) { + if (file == null) throw new IllegalArgumentException("file = null"); + if (!file.exists()) { + return -1; + } + for (int i = 0; i < servedFiles.size(); i++) { + if (servedFiles.get(i).getAbsolutePath().equals(file.getAbsolutePath())) { + return i; + } + } + servedFiles.add(file); + return servedFiles.size() - 1; + } + + /** + * Removes the file with the given ID from the server. + * + * @return True if a file was removed, false otherwise + */ + public synchronized boolean removeFile(int id) { + if (id < 0) throw new IllegalArgumentException("ID < 0"); + if (id >= servedFiles.size()) { + return false; + } else { + return servedFiles.remove(id) != null; + } + } + + public synchronized File accessFile(int id) { + if (id < 0 || id >= servedFiles.size()) { + return null; + } else { + return servedFiles.get(id); + } + } + + @Override + public Response serve(IHTTPSession session) { + + if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.getUri()); + + String[] segments = session.getUri().split("/"); + if (segments.length < 3) { + Log.w(TAG, String.format("Invalid number of URI segments: %d %s", segments.length, Arrays.toString(segments))); + get404Error(); + } + + final String func = segments[1]; + final String param = segments[2]; + final Map<String, String> headers = session.getHeaders(); + + if (func.equalsIgnoreCase("status")) { + try { + int code = Integer.parseInt(param); + return getStatus(code); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } + + } else if (func.equalsIgnoreCase("redirect")) { + try { + int times = Integer.parseInt(param); + if (times < 0) { + throw new NumberFormatException("times <= 0: " + times); + } + + return getRedirectResponse(times - 1); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } + } else if (func.equalsIgnoreCase("delay")) { + try { + int sec = Integer.parseInt(param); + if (sec <= 0) { + throw new NumberFormatException("sec <= 0: " + sec); + } + + Thread.sleep(sec * 1000L); + return getOKResponse(); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } catch (InterruptedException e) { + e.printStackTrace(); + return getInternalError(); + } + } else if (func.equalsIgnoreCase("basic-auth")) { + if (!headers.containsKey("authorization")) { + Log.w(TAG, "No credentials provided"); + return getUnauthorizedResponse(); + } + try { + String credentials = new String(Base64.decode(headers.get("authorization").split(" ")[1], 0), "UTF-8"); + String[] credentialParts = credentials.split(":"); + if (credentialParts.length != 2) { + Log.w(TAG, "Unable to split credentials: " + Arrays.toString(credentialParts)); + return getInternalError(); + } + if (credentialParts[0].equals(segments[2]) + && credentialParts[1].equals(segments[3])) { + Log.i(TAG, "Credentials accepted"); + return getOKResponse(); + } else { + Log.w(TAG, String.format("Invalid credentials. Expected %s, %s, but was %s, %s", + segments[2], segments[3], credentialParts[0], credentialParts[1])); + return getUnauthorizedResponse(); + } + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return getInternalError(); + } + } else if (func.equalsIgnoreCase("gzip")) { + try { + int size = Integer.parseInt(param); + if (size <= 0) { + Log.w(TAG, "Invalid size for gzipped data: " + size); + throw new NumberFormatException(); + } + + return getGzippedResponse(size); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } catch (IOException e) { + e.printStackTrace(); + return getInternalError(); + } + } else if (func.equalsIgnoreCase("files")) { + try { + int id = Integer.parseInt(param); + if (id < 0) { + Log.w(TAG, "Invalid ID: " + id); + throw new NumberFormatException(); + } + return getFileAccessResponse(id, headers); + + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } + } + + return get404Error(); + } + + private synchronized Response getFileAccessResponse(int id, Map<String, String> header) { + File file = accessFile(id); + if (file == null || !file.exists()) { + Log.w(TAG, "File not found: " + id); + return get404Error(); + } + InputStream inputStream = null; + String contentRange = null; + Response.Status status; + boolean successful = false; + try { + inputStream = new FileInputStream(file); + if (header.containsKey("range")) { + // read range header field + final String value = header.get("range"); + final String[] segments = value.split("="); + if (segments.length != 2) { + Log.w(TAG, "Invalid segment length: " + Arrays.toString(segments)); + return getInternalError(); + } + final String type = StringUtils.substringBefore(value, "="); + if (!type.equalsIgnoreCase("bytes")) { + Log.w(TAG, "Range is not specified in bytes: " + value); + return getInternalError(); + } + try { + long start = Long.parseLong(StringUtils.substringBefore(segments[1], "-")); + if (start >= file.length()) { + return getRangeNotSatisfiable(); + } + + // skip 'start' bytes + IOUtils.skipFully(inputStream, start); + contentRange = "bytes " + start + (file.length() - 1) + "/" + file.length(); + + } catch (NumberFormatException e) { + e.printStackTrace(); + return getInternalError(); + } catch (IOException e) { + e.printStackTrace(); + return getInternalError(); + } + + status = Response.Status.PARTIAL_CONTENT; + + } else { + // request did not contain range header field + status = Response.Status.OK; + } + successful = true; + } catch (FileNotFoundException e) { + e.printStackTrace(); + + return getInternalError(); + } finally { + if (!successful && inputStream != null) { + IOUtils.closeQuietly(inputStream); + } + } + + Response response = new Response(status, URLConnection.guessContentTypeFromName(file.getAbsolutePath()), inputStream); + + response.addHeader("Accept-Ranges", "bytes"); + if (contentRange != null) { + response.addHeader("Content-Range", contentRange); + } + response.addHeader("Content-Length", String.valueOf(file.length())); + return response; + } + + private Response getGzippedResponse(int size) throws IOException { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + final byte[] buffer = new byte[size]; + Random random = new Random(System.currentTimeMillis()); + random.nextBytes(buffer); + + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed); + gzipOutputStream.write(buffer); + + InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray()); + Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream); + response.addHeader("Content-encoding", "gzip"); + response.addHeader("Content-length", String.valueOf(compressed.size())); + return response; + } + + private Response getStatus(final int code) { + Response.IStatus status = (code == 200) ? Response.Status.OK : + (code == 201) ? Response.Status.CREATED : + (code == 206) ? Response.Status.PARTIAL_CONTENT : + (code == 301) ? Response.Status.REDIRECT : + (code == 304) ? Response.Status.NOT_MODIFIED : + (code == 400) ? Response.Status.BAD_REQUEST : + (code == 401) ? Response.Status.UNAUTHORIZED : + (code == 403) ? Response.Status.FORBIDDEN : + (code == 404) ? Response.Status.NOT_FOUND : + (code == 405) ? Response.Status.METHOD_NOT_ALLOWED : + (code == 416) ? Response.Status.RANGE_NOT_SATISFIABLE : + (code == 500) ? Response.Status.INTERNAL_ERROR : new Response.IStatus() { + @Override + public int getRequestStatus() { + return code; + } + + @Override + public String getDescription() { + return "Unknown"; + } + }; + return new Response(status, MIME_HTML, ""); + + } + + private Response getRedirectResponse(int times) { + if (times > 0) { + Response response = new Response(Response.Status.REDIRECT, MIME_HTML, "This resource has been moved permanently"); + response.addHeader("Location", "/redirect/" + times); + return response; + } else if (times == 0) { + return getOKResponse(); + } else { + return getInternalError(); + } + } + + private Response getUnauthorizedResponse() { + Response response = new Response(Response.Status.UNAUTHORIZED, MIME_HTML, ""); + response.addHeader("WWW-Authenticate", "Basic realm=\"Test Realm\""); + return response; + } + + private Response getOKResponse() { + return new Response(Response.Status.OK, MIME_HTML, ""); + } + + private Response getInternalError() { + return new Response(Response.Status.INTERNAL_ERROR, MIME_HTML, "The server encountered an internal error"); + } + + private Response getRangeNotSatisfiable() { + return new Response(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAIN, ""); + } + + private Response get404Error() { + return new Response(Response.Status.NOT_FOUND, MIME_HTML, "The requested URL was not found on this server"); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java new file mode 100644 index 000000000..4a5818479 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java @@ -0,0 +1,1420 @@ +package de.test.antennapod.util.service.download; + +import java.io.*; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TimeZone; + +/** + * A simple, tiny, nicely embeddable HTTP server in Java + * <p/> + * <p/> + * NanoHTTPD + * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p> + * <p/> + * <p/> + * <b>Features + limitations: </b> + * <ul> + * <p/> + * <li>Only one Java file</li> + * <li>Java 5 compatible</li> + * <li>Released as open source, Modified BSD licence</li> + * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li> + * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li> + * <li>Supports both dynamic content and file serving</li> + * <li>Supports file upload (since version 1.2, 2010)</li> + * <li>Supports partial content (streaming)</li> + * <li>Supports ETags</li> + * <li>Never caches anything</li> + * <li>Doesn't limit bandwidth, request time or simultaneous connections</li> + * <li>Default code serves files and shows all HTTP parameters and headers</li> + * <li>File server supports directory listing, index.html and index.htm</li> + * <li>File server supports partial content (streaming)</li> + * <li>File server supports ETags</li> + * <li>File server does the 301 redirection trick for directories without '/'</li> + * <li>File server supports simple skipping for files (continue download)</li> + * <li>File server serves also very long files without memory overhead</li> + * <li>Contains a built-in list of most common mime types</li> + * <li>All header names are converted lowercase so they don't vary between browsers/clients</li> + * <p/> + * </ul> + * <p/> + * <p/> + * <b>How to use: </b> + * <ul> + * <p/> + * <li>Subclass and implement serve() and embed to your own program</li> + * <p/> + * </ul> + * <p/> + * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence) + */ +public abstract class NanoHTTPD { + /** + * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) + * This is required as the Keep-Alive HTTP connections would otherwise + * block the socket reading thread forever (or as long the browser is open). + */ + public static final int SOCKET_READ_TIMEOUT = 5000; + /** + * Common mime type for dynamic content: plain text + */ + public static final String MIME_PLAINTEXT = "text/plain"; + /** + * Common mime type for dynamic content: html + */ + public static final String MIME_HTML = "text/html"; + /** + * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + private final String hostname; + private final int myPort; + private ServerSocket myServerSocket; + private Set<Socket> openConnections = new HashSet<Socket>(); + private Thread myThread; + /** + * Pluggable strategy for asynchronously executing requests. + */ + private AsyncRunner asyncRunner; + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private TempFileManagerFactory tempFileManagerFactory; + + /** + * Constructs an HTTP server on given port. + */ + public NanoHTTPD(int port) { + this(null, port); + } + + /** + * Constructs an HTTP server on given hostname and port. + */ + public NanoHTTPD(String hostname, int port) { + this.hostname = hostname; + this.myPort = port; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + setAsyncRunner(new DefaultAsyncRunner()); + } + + private static final void safeClose(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + } + } + } + + private static final void safeClose(Socket closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + } + } + } + + private static final void safeClose(ServerSocket closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + } + } + } + + /** + * Start the server. + * + * @throws IOException if the socket is in use. + */ + public void start() throws IOException { + myServerSocket = new ServerSocket(); + myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); + + myThread = new Thread(new Runnable() { + @Override + public void run() { + do { + try { + final Socket finalAccept = myServerSocket.accept(); + registerConnection(finalAccept); + finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT); + final InputStream inputStream = finalAccept.getInputStream(); + asyncRunner.exec(new Runnable() { + @Override + public void run() { + OutputStream outputStream = null; + try { + outputStream = finalAccept.getOutputStream(); + TempFileManager tempFileManager = tempFileManagerFactory.create(); + HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress()); + while (!finalAccept.isClosed()) { + session.execute(); + } + } catch (Exception e) { + // When the socket is closed by the client, we throw our own SocketException + // to break the "keep alive" loop above. + if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) { + e.printStackTrace(); + } + } finally { + safeClose(outputStream); + safeClose(inputStream); + safeClose(finalAccept); + unRegisterConnection(finalAccept); + } + } + }); + } catch (IOException e) { + } + } while (!myServerSocket.isClosed()); + } + }); + myThread.setDaemon(true); + myThread.setName("NanoHttpd Main Listener"); + myThread.start(); + } + + /** + * Stop the server. + */ + public void stop() { + try { + safeClose(myServerSocket); + closeAllConnections(); + if (myThread != null) { + myThread.join(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Registers that a new connection has been set up. + * + * @param socket the {@link Socket} for the connection. + */ + public synchronized void registerConnection(Socket socket) { + openConnections.add(socket); + } + + /** + * Registers that a connection has been closed + * + * @param socket + * the {@link Socket} for the connection. + */ + public synchronized void unRegisterConnection(Socket socket) { + openConnections.remove(socket); + } + + /** + * Forcibly closes all connections that are open. + */ + public synchronized void closeAllConnections() { + for (Socket socket : openConnections) { + safeClose(socket); + } + } + + public final int getListeningPort() { + return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); + } + + public final boolean wasStarted() { + return myServerSocket != null && myThread != null; + } + + public final boolean isAlive() { + return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive(); + } + + /** + * Override this to customize the server. + * <p/> + * <p/> + * (By default, this delegates to serveFile() and allows directory listing.) + * + * @param uri Percent-decoded URI without parameters, for example "/index.cgi" + * @param method "GET", "POST" etc. + * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. + * @param headers Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + @Deprecated + public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, + Map<String, String> files) { + return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + } + + /** + * Override this to customize the server. + * <p/> + * <p/> + * (By default, this delegates to serveFile() and allows directory listing.) + * + * @param session The HTTP session + * @return HTTP response, see class Response for details + */ + public Response serve(IHTTPSession session) { + Map<String, String> files = new HashMap<String, String>(); + Method method = session.getMethod(); + if (Method.PUT.equals(method) || Method.POST.equals(method)) { + try { + session.parseBody(files); + } catch (IOException ioe) { + return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (ResponseException re) { + return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); + } + } + + Map<String, String> parms = session.getParms(); + parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString()); + return serve(session.getUri(), method, session.getHeaders(), parms, files); + } + + /** + * Decode percent encoded <code>String</code> values. + * + * @param str the percent encoded <code>String</code> + * @return expanded form of the input, for example "foo%20bar" becomes "foo bar" + */ + protected String decodePercent(String str) { + String decoded = null; + try { + decoded = URLDecoder.decode(str, "UTF8"); + } catch (UnsupportedEncodingException ignored) { + } + return decoded; + } + + /** + * Decode parameters from a URL, handing the case where a single parameter name might have been + * supplied several times, by return lists of values. In general these lists will contain a single + * element. + * + * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method. + * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). + */ + protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { + return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER)); + } + + /** + * Decode parameters from a URL, handing the case where a single parameter name might have been + * supplied several times, by return lists of values. In general these lists will contain a single + * element. + * + * @param queryString a query string pulled from the URL. + * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). + */ + protected Map<String, List<String>> decodeParameters(String queryString) { + Map<String, List<String>> parms = new HashMap<String, List<String>>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList<String>()); + } + String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } + } + return parms; + } + + // ------------------------------------------------------------------------------- // + // + // Threading Strategy. + // + // ------------------------------------------------------------------------------- // + + /** + * Pluggable strategy for asynchronously executing requests. + * + * @param asyncRunner new strategy for handling threads. + */ + public void setAsyncRunner(AsyncRunner asyncRunner) { + this.asyncRunner = asyncRunner; + } + + // ------------------------------------------------------------------------------- // + // + // Temp file handling strategy. + // + // ------------------------------------------------------------------------------- // + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory new strategy for handling temp files. + */ + public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value. + */ + public enum Method { + GET, PUT, POST, DELETE, HEAD, OPTIONS; + + static Method lookup(String method) { + for (Method m : Method.values()) { + if (m.toString().equalsIgnoreCase(method)) { + return m; + } + } + return null; + } + } + + /** + * Pluggable strategy for asynchronously executing requests. + */ + public interface AsyncRunner { + void exec(Runnable code); + } + + /** + * Factory to create temp file managers. + */ + public interface TempFileManagerFactory { + TempFileManager create(); + } + + // ------------------------------------------------------------------------------- // + + /** + * Temp file manager. + * <p/> + * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup + * temporary files created as a result of handling the request.</p> + */ + public interface TempFileManager { + TempFile createTempFile() throws Exception; + + void clear(); + } + + /** + * A temp file. + * <p/> + * <p>Temp files are responsible for managing the actual temporary storage and cleaning + * themselves up when no longer needed.</p> + */ + public interface TempFile { + OutputStream open() throws Exception; + + void delete() throws Exception; + + String getName(); + } + + /** + * Default threading strategy for NanoHttpd. + * <p/> + * <p>By default, the server spawns a new Thread for every incoming request. These are set + * to <i>daemon</i> status, and named according to the request number. The name is + * useful when profiling the application.</p> + */ + public static class DefaultAsyncRunner implements AsyncRunner { + private long requestCount; + + @Override + public void exec(Runnable code) { + ++requestCount; + Thread t = new Thread(code); + t.setDaemon(true); + t.setName("NanoHttpd Request Processor (#" + requestCount + ")"); + t.start(); + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + * <p/> + * <p></p>This class stores its files in the standard location (that is, + * wherever <code>java.io.tmpdir</code> points to). Files are added + * to an internal list, and deleted when no longer needed (that is, + * when <code>clear()</code> is invoked at the end of processing a + * request).</p> + */ + public static class DefaultTempFileManager implements TempFileManager { + private final String tmpdir; + private final List<TempFile> tempFiles; + + public DefaultTempFileManager() { + tmpdir = System.getProperty("java.io.tmpdir"); + tempFiles = new ArrayList<TempFile>(); + } + + @Override + public TempFile createTempFile() throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(tmpdir); + tempFiles.add(tempFile); + return tempFile; + } + + @Override + public void clear() { + for (TempFile file : tempFiles) { + try { + file.delete(); + } catch (Exception ignored) { + } + } + tempFiles.clear(); + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + * <p/> + * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in + * the directory specified.</p> + */ + public static class DefaultTempFile implements TempFile { + private File file; + private OutputStream fstream; + + public DefaultTempFile(String tempdir) throws IOException { + file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); + fstream = new FileOutputStream(file); + } + + @Override + public OutputStream open() throws Exception { + return fstream; + } + + @Override + public void delete() throws Exception { + safeClose(fstream); + file.delete(); + } + + @Override + public String getName() { + return file.getAbsolutePath(); + } + } + + /** + * HTTP response. Return one of these from serve(). + */ + public static class Response { + /** + * HTTP status code after processing, e.g. "200 OK", HTTP_OK + */ + private IStatus status; + /** + * MIME type of content, e.g. "text/html" + */ + private String mimeType; + /** + * Data of the response, may be null. + */ + private InputStream data; + /** + * Headers for the HTTP response. Use addHeader() to add lines. + */ + private Map<String, String> header = new HashMap<String, String>(); + /** + * The request method that spawned this response. + */ + private Method requestMethod; + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + /** + * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message + */ + public Response(String msg) { + this(Status.OK, MIME_HTML, msg); + } + + /** + * Basic constructor. + */ + public Response(IStatus status, String mimeType, InputStream data) { + this.status = status; + this.mimeType = mimeType; + this.data = data; + } + + /** + * Convenience method that makes an InputStream out of given text. + */ + public Response(IStatus status, String mimeType, String txt) { + this.status = status; + this.mimeType = mimeType; + try { + this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null; + } catch (java.io.UnsupportedEncodingException uee) { + uee.printStackTrace(); + } + } + + /** + * Adds given line to the header. + */ + public void addHeader(String name, String value) { + header.put(name, value); + } + + public String getHeader(String name) { + return header.get(name); + } + + /** + * Sends given response to the socket. + */ + protected void send(OutputStream outputStream) { + String mime = mimeType; + SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + + try { + if (status == null) { + throw new Error("sendResponse(): Status can't be null."); + } + PrintWriter pw = new PrintWriter(outputStream); + pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); + + if (mime != null) { + pw.print("Content-Type: " + mime + "\r\n"); + } + + if (header == null || header.get("Date") == null) { + pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); + } + + if (header != null) { + for (String key : header.keySet()) { + String value = header.get(key); + pw.print(key + ": " + value + "\r\n"); + } + } + + sendConnectionHeaderIfNotAlreadyPresent(pw, header); + + if (requestMethod != Method.HEAD && chunkedTransfer) { + sendAsChunked(outputStream, pw); + } else { + int pending = data != null ? data.available() : 0; + sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending); + pw.print("\r\n"); + pw.flush(); + sendAsFixedLength(outputStream, pending); + } + outputStream.flush(); + safeClose(data); + } catch (IOException ioe) { + // Couldn't write? No can do. + } + } + + protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) { + if (!headerAlreadySent(header, "content-length")) { + pw.print("Content-Length: "+ size +"\r\n"); + } + } + + protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) { + if (!headerAlreadySent(header, "connection")) { + pw.print("Connection: keep-alive\r\n"); + } + } + + private boolean headerAlreadySent(Map<String, String> header, String name) { + boolean alreadySent = false; + for (String headerName : header.keySet()) { + alreadySent |= headerName.equalsIgnoreCase(name); + } + return alreadySent; + } + + private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { + pw.print("Transfer-Encoding: chunked\r\n"); + pw.print("\r\n"); + pw.flush(); + int BUFFER_SIZE = 16 * 1024; + byte[] CRLF = "\r\n".getBytes(); + byte[] buff = new byte[BUFFER_SIZE]; + int read; + while ((read = data.read(buff)) > 0) { + outputStream.write(String.format("%x\r\n", read).getBytes()); + outputStream.write(buff, 0, read); + outputStream.write(CRLF); + } + outputStream.write(String.format("0\r\n\r\n").getBytes()); + } + + private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException { + if (requestMethod != Method.HEAD && data != null) { + int BUFFER_SIZE = 16 * 1024; + byte[] buff = new byte[BUFFER_SIZE]; + while (pending > 0) { + int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); + if (read <= 0) { + break; + } + outputStream.write(buff, 0, read); + pending -= read; + } + } + } + + public IStatus getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public InputStream getData() { + return data; + } + + public void setData(InputStream data) { + this.data = data; + } + + public Method getRequestMethod() { + return requestMethod; + } + + public void setRequestMethod(Method requestMethod) { + this.requestMethod = requestMethod; + } + + public void setChunkedTransfer(boolean chunkedTransfer) { + this.chunkedTransfer = chunkedTransfer; + } + + public interface IStatus { + int getRequestStatus(); + String getDescription(); + } + + /** + * Some HTTP response status codes + */ + public enum Status implements IStatus { + SWITCH_PROTOCOL(101, "Switching Protocols"), OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301, + "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, + "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), RANGE_NOT_SATISFIABLE(416, + "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error"); + private final int requestStatus; + private final String description; + + Status(int requestStatus, String description) { + this.requestStatus = requestStatus; + this.description = description; + } + + @Override + public int getRequestStatus() { + return this.requestStatus; + } + + @Override + public String getDescription() { + return "" + this.requestStatus + " " + description; + } + } + } + + public static final class ResponseException extends Exception { + + private final Response.Status status; + + public ResponseException(Response.Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Response.Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Response.Status getStatus() { + return status; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + */ + private class DefaultTempFileManagerFactory implements TempFileManagerFactory { + @Override + public TempFileManager create() { + return new DefaultTempFileManager(); + } + } + + /** + * Handles one session, i.e. parses the HTTP request and returns the response. + */ + public interface IHTTPSession { + void execute() throws IOException; + + Map<String, String> getParms(); + + Map<String, String> getHeaders(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + String getQueryParameterString(); + + Method getMethod(); + + InputStream getInputStream(); + + CookieHandler getCookies(); + + /** + * Adds the files in the request body to the files map. + * @arg files - map to modify + */ + void parseBody(Map<String, String> files) throws IOException, ResponseException; + } + + protected class HTTPSession implements IHTTPSession { + public static final int BUFSIZE = 8192; + private final TempFileManager tempFileManager; + private final OutputStream outputStream; + private PushbackInputStream inputStream; + private int splitbyte; + private int rlen; + private String uri; + private Method method; + private Map<String, String> parms; + private Map<String, String> headers; + private CookieHandler cookies; + private String queryParameterString; + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.tempFileManager = tempFileManager; + this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); + this.outputStream = outputStream; + } + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + this.tempFileManager = tempFileManager; + this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); + this.outputStream = outputStream; + String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); + headers = new HashMap<String, String>(); + + headers.put("remote-addr", remoteIp); + headers.put("http-client-ip", remoteIp); + } + + @Override + public void execute() throws IOException { + try { + // Read the first 8192 bytes. + // The full header should fit in here. + // Apache's default header limit is 8KB. + // Do NOT assume that a single read will get the entire header at once! + byte[] buf = new byte[BUFSIZE]; + splitbyte = 0; + rlen = 0; + { + int read = -1; + try { + read = inputStream.read(buf, 0, BUFSIZE); + } catch (Exception e) { + safeClose(inputStream); + safeClose(outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + if (read == -1) { + // socket was been closed + safeClose(inputStream); + safeClose(outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + while (read > 0) { + rlen += read; + splitbyte = findHeaderEnd(buf, rlen); + if (splitbyte > 0) + break; + read = inputStream.read(buf, rlen, BUFSIZE - rlen); + } + } + + if (splitbyte < rlen) { + inputStream.unread(buf, splitbyte, rlen - splitbyte); + } + + parms = new HashMap<String, String>(); + if(null == headers) { + headers = new HashMap<String, String>(); + } + + // Create a BufferedReader for parsing the header. + BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); + + // Decode the header into parms and header java properties + Map<String, String> pre = new HashMap<String, String>(); + decodeHeader(hin, pre, parms, headers); + + method = Method.lookup(pre.get("method")); + if (method == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); + } + + uri = pre.get("uri"); + + cookies = new CookieHandler(headers); + + // Ok, now do the serve() + Response r = serve(this); + if (r == null) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + cookies.unloadQueue(r); + r.setRequestMethod(method); + r.send(outputStream); + } + } catch (SocketException e) { + // throw it out to close socket object (finalAccept) + throw e; + } catch (SocketTimeoutException ste) { + throw ste; + } catch (IOException ioe) { + Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + r.send(outputStream); + safeClose(outputStream); + } catch (ResponseException re) { + Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); + r.send(outputStream); + safeClose(outputStream); + } finally { + tempFileManager.clear(); + } + } + + @Override + public void parseBody(Map<String, String> files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + BufferedReader in = null; + try { + + randomAccessFile = getTmpBucket(); + + long size; + if (headers.containsKey("content-length")) { + size = Integer.parseInt(headers.get("content-length")); + } else if (splitbyte < rlen) { + size = rlen - splitbyte; + } else { + size = 0; + } + + // Now read all the body and write it to f + byte[] buf = new byte[512]; + while (rlen >= 0 && size > 0) { + rlen = inputStream.read(buf, 0, (int)Math.min(size, 512)); + size -= rlen; + if (rlen > 0) { + randomAccessFile.write(buf, 0, rlen); + } + } + + // Get the raw body as a byte [] + ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); + randomAccessFile.seek(0); + + // Create a BufferedReader for easily reading it as string. + InputStream bin = new FileInputStream(randomAccessFile.getFD()); + in = new BufferedReader(new InputStreamReader(bin)); + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (Method.POST.equals(method)) { + String contentType = ""; + String contentTypeHeader = headers.get("content-type"); + + StringTokenizer st = null; + if (contentTypeHeader != null) { + st = new StringTokenizer(contentTypeHeader, ",; "); + if (st.hasMoreTokens()) { + contentType = st.nextToken(); + } + } + + if ("multipart/form-data".equalsIgnoreCase(contentType)) { + // Handle multipart/form-data + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + } + + String boundaryStartString = "boundary="; + int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); + String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); + if (boundary.startsWith("\"") && boundary.endsWith("\"")) { + boundary = boundary.substring(1, boundary.length() - 1); + } + + decodeMultipartData(boundary, fbuf, in, parms, files); + } else { + String postLine = ""; + StringBuilder postLineBuffer = new StringBuilder(); + char pbuf[] = new char[512]; + int read = in.read(pbuf); + while (read >= 0 && !postLine.endsWith("\r\n")) { + postLine = String.valueOf(pbuf, 0, read); + postLineBuffer.append(postLine); + read = in.read(pbuf); + } + postLine = postLineBuffer.toString().trim(); + // Handle application/x-www-form-urlencoded + if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { + decodeParms(postLine, parms); + } else if (postLine.length() != 0) { + // Special case for raw POST data => create a special files entry "postData" with raw content data + files.put("postData", postLine); + } + } + } else if (Method.PUT.equals(method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); + } + } finally { + safeClose(randomAccessFile); + safeClose(in); + } + } + + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) + throws ResponseException { + try { + // Read the request line + String inLine = in.readLine(); + if (inLine == null) { + return; + } + + StringTokenizer st = new StringTokenizer(inLine); + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + pre.put("method", st.nextToken()); + + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + } + + String uri = st.nextToken(); + + // Decode parameters from the URI + int qmi = uri.indexOf('?'); + if (qmi >= 0) { + decodeParms(uri.substring(qmi + 1), parms); + uri = decodePercent(uri.substring(0, qmi)); + } else { + uri = decodePercent(uri); + } + + // If there's another token, it's protocol version, + // followed by HTTP headers. Ignore version but parse headers. + // NOTE: this now forces header names lowercase since they are + // case insensitive and vary by client. + if (st.hasMoreTokens()) { + String line = in.readLine(); + while (line != null && line.trim().length() > 0) { + int p = line.indexOf(':'); + if (p >= 0) + headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); + line = in.readLine(); + } + } + + pre.put("uri", uri); + } catch (IOException ioe) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Decodes the Multipart Body data and put it into Key/Value pairs. + */ + private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, + Map<String, String> files) throws ResponseException { + try { + int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); + int boundarycount = 1; + String mpline = in.readLine(); + while (mpline != null) { + if (!mpline.contains(boundary)) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); + } + boundarycount++; + Map<String, String> item = new HashMap<String, String>(); + mpline = in.readLine(); + while (mpline != null && mpline.trim().length() > 0) { + int p = mpline.indexOf(':'); + if (p != -1) { + item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); + } + mpline = in.readLine(); + } + if (mpline != null) { + String contentDisposition = item.get("content-disposition"); + if (contentDisposition == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); + } + StringTokenizer st = new StringTokenizer(contentDisposition, ";"); + Map<String, String> disposition = new HashMap<String, String>(); + while (st.hasMoreTokens()) { + String token = st.nextToken().trim(); + int p = token.indexOf('='); + if (p != -1) { + disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); + } + } + String pname = disposition.get("name"); + pname = pname.substring(1, pname.length() - 1); + + String value = ""; + if (item.get("content-type") == null) { + while (mpline != null && !mpline.contains(boundary)) { + mpline = in.readLine(); + if (mpline != null) { + int d = mpline.indexOf(boundary); + if (d == -1) { + value += mpline; + } else { + value += mpline.substring(0, d - 2); + } + } + } + } else { + if (boundarycount > bpositions.length) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); + } + int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); + String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); + files.put(pname, path); + value = disposition.get("filename"); + value = value.substring(1, value.length() - 1); + do { + mpline = in.readLine(); + } while (mpline != null && !mpline.contains(boundary)); + } + parms.put(pname, value); + } + } + } catch (IOException ioe) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. + */ + private int findHeaderEnd(final byte[] buf, int rlen) { + int splitbyte = 0; + while (splitbyte + 3 < rlen) { + if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { + return splitbyte + 4; + } + splitbyte++; + } + return 0; + } + + /** + * Find the byte positions where multipart boundaries start. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int matchcount = 0; + int matchbyte = -1; + List<Integer> matchbytes = new ArrayList<Integer>(); + for (int i = 0; i < b.limit(); i++) { + if (b.get(i) == boundary[matchcount]) { + if (matchcount == 0) + matchbyte = i; + matchcount++; + if (matchcount == boundary.length) { + matchbytes.add(matchbyte); + matchcount = 0; + matchbyte = -1; + } + } else { + i -= matchcount; + matchcount = 0; + matchbyte = -1; + } + } + int[] ret = new int[matchbytes.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = matchbytes.get(i); + } + return ret; + } + + /** + * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. + */ + private String saveTmpFile(ByteBuffer b, int offset, int len) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + TempFile tempFile = tempFileManager.createTempFile(); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + safeClose(fileOutputStream); + } + } + return path; + } + + private RandomAccessFile getTmpBucket() { + try { + TempFile tempFile = tempFileManager.createTempFile(); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } + + /** + * It returns the offset separating multipart file headers from the file's data. + */ + private int stripMultipartHeaders(ByteBuffer b, int offset) { + int i; + for (i = offset; i < b.limit(); i++) { + if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { + break; + } + } + return i + 1; + } + + /** + * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and + * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map. + */ + private void decodeParms(String parms, Map<String, String> p) { + if (parms == null) { + queryParameterString = ""; + return; + } + + queryParameterString = parms; + StringTokenizer st = new StringTokenizer(parms, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + if (sep >= 0) { + p.put(decodePercent(e.substring(0, sep)).trim(), + decodePercent(e.substring(sep + 1))); + } else { + p.put(decodePercent(e).trim(), ""); + } + } + } + + @Override + public final Map<String, String> getParms() { + return parms; + } + + public String getQueryParameterString() { + return queryParameterString; + } + + @Override + public final Map<String, String> getHeaders() { + return headers; + } + + @Override + public final String getUri() { + return uri; + } + + @Override + public final Method getMethod() { + return method; + } + + @Override + public final InputStream getInputStream() { + return inputStream; + } + + @Override + public CookieHandler getCookies() { + return cookies; + } + } + + public static class Cookie { + private String n, v, e; + + public Cookie(String name, String value, String expires) { + n = name; + v = value; + e = expires; + } + + public Cookie(String name, String value) { + this(name, value, 30); + } + + public Cookie(String name, String value, int numDays) { + n = name; + v = value; + e = getHTTPTime(numDays); + } + + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, n, v, e); + } + + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); + } + } + + /** + * Provides rudimentary support for cookies. + * Doesn't support 'path', 'secure' nor 'httpOnly'. + * Feel free to improve it and/or add unsupported features. + * + * @author LordFokas + */ + public class CookieHandler implements Iterable<String> { + private HashMap<String, String> cookies = new HashMap<String, String>(); + private ArrayList<Cookie> queue = new ArrayList<Cookie>(); + + public CookieHandler(Map<String, String> httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + cookies.put(data[0], data[1]); + } + } + } + } + + @Override public Iterator<String> iterator() { + return cookies.keySet().iterator(); + } + + /** + * Read a cookie from the HTTP Headers. + * + * @param name The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return cookies.get(name); + } + + /** + * Sets a cookie. + * + * @param name The cookie's name. + * @param value The cookie's value. + * @param expires How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + public void set(Cookie cookie) { + queue.add(cookie); + } + + /** + * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side. + * + * @param name The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } + + /** + * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers. + * + * @param response The Response object to which headers the queued cookies will be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : queue) { + response.addHeader("Set-Cookie", cookie.getHTTPHeader()); + } + } + } +} 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 new file mode 100644 index 000000000..4e5d0297f --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java @@ -0,0 +1,109 @@ +package de.test.antennapod.util.syndication; + +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Map; + +/** + * Test class for FeedDiscoverer + */ +public class FeedDiscovererTest extends InstrumentationTestCase { + + private FeedDiscoverer fd; + + private File testDir; + + @Override + public void setUp() throws Exception { + super.setUp(); + fd = new FeedDiscoverer(); + testDir = getInstrumentation().getTargetContext().getExternalFilesDir("FeedDiscovererTest"); + testDir.mkdir(); + assertTrue(testDir.exists()); + } + + @Override + protected void tearDown() throws Exception { + FileUtils.deleteDirectory(testDir); + super.tearDown(); + } + + 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); + 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)); + } + } + + public void testAlternateRSSWithTitleAbsolute() throws Exception { + checkFindUrls(true, true, true, true, true); + } + + public void testAlternateRSSWithTitleRelative() throws Exception { + checkFindUrls(true, true, true, false, true); + } + + public void testAlternateRSSNoTitleAbsolute() throws Exception { + checkFindUrls(true, true, false, true, true); + } + + public void testAlternateRSSNoTitleRelative() throws Exception { + checkFindUrls(true, true, false, false, true); + } + + public void testAlternateAtomWithTitleAbsolute() throws Exception { + checkFindUrls(true, false, true, true, true); + } + + public void testFeedAtomWithTitleAbsolute() throws Exception { + checkFindUrls(false, false, true, true, true); + } + + 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 new file mode 100644 index 000000000..69cc827ec --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java @@ -0,0 +1,118 @@ +package de.test.antennapod.util.syndication.feedgenerator; + +import android.util.Xml; +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.util.SyndDateUtils; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * 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"; + + public 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.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(SyndDateUtils.formatRFC3339Local(item.getPubDate())); + } else { + xml.text(SyndDateUtils.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"); + } + } + + xml.endTag(null, "feed"); + xml.endDocument(); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java new file mode 100644 index 000000000..fe5afd847 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java @@ -0,0 +1,28 @@ +package de.test.antennapod.util.syndication.feedgenerator; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Generates a machine-readable, platform-independent representation of a Feed object. + */ +public interface FeedGenerator { + + /** + * Creates a machine-readable, platform-independent representation of a given + * Feed object and writes it to the given OutputStream. + * <p/> + * The representation might not be compliant with its specification if the feed + * is missing certain attribute values. This is intentional because the FeedGenerator is + * used for creating test data. + * + * @param feed The feed that should be written. Must not be null. + * @param outputStream The output target that the feed will be written to. The outputStream is not closed after + * the method's execution Must not be null. + * @param encoding The encoding to use. Must not be null. + * @param flags Optional argument for enabling implementation-dependent features. + */ + public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException; +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java new file mode 100644 index 000000000..e7cbb1b42 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java @@ -0,0 +1,21 @@ +package de.test.antennapod.util.syndication.feedgenerator; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + +/** + * Utility methods for FeedGenerator + */ +public class GeneratorUtil { + + public static void addPaymentLink(XmlSerializer xml, String paymentLink, boolean withNamespace) throws IOException { + String ns = (withNamespace) ? "http://www.w3.org/2005/Atom" : null; + xml.startTag(ns, "link"); + xml.attribute(null, "rel", "payment"); + xml.attribute(null, "title", "Flattr this!"); + xml.attribute(null, "href", paymentLink); + xml.attribute(null, "type", "text/html"); + xml.endTag(ns, "link"); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java new file mode 100644 index 000000000..d37434f06 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java @@ -0,0 +1,110 @@ +package de.test.antennapod.util.syndication.feedgenerator; + +import android.util.Xml; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Creates RSS 2.0 feeds. See FeedGenerator for more information. + */ +public class RSS2Generator implements FeedGenerator{ + + public static final long FEATURE_WRITE_GUID = 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.setPrefix("atom", "http://www.w3.org/2005/Atom"); + xml.startTag(null, "rss"); + xml.attribute(null, "version", "2.0"); + xml.startTag(null, "channel"); + + // Write Feed data + if (feed.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(feed.getTitle()); + xml.endTag(null, "title"); + } + if (feed.getDescription() != null) { + xml.startTag(null, "description"); + xml.text(feed.getDescription()); + xml.endTag(null, "description"); + } + if (feed.getLink() != null) { + xml.startTag(null, "link"); + xml.text(feed.getLink()); + xml.endTag(null, "link"); + } + if (feed.getLanguage() != null) { + xml.startTag(null, "language"); + xml.text(feed.getLanguage()); + xml.endTag(null, "language"); + } + + if (feed.getPaymentLink() != null) { + GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), true); + } + + // Write FeedItem data + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + xml.startTag(null, "item"); + + if (item.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(item.getTitle()); + xml.endTag(null, "title"); + } + if (item.getDescription() != null) { + xml.startTag(null, "description"); + xml.text(item.getDescription()); + xml.endTag(null, "description"); + } + if (item.getLink() != null) { + xml.startTag(null, "link"); + xml.text(item.getLink()); + xml.endTag(null, "link"); + } + if (item.getPubDate() != null) { + xml.startTag(null, "pubDate"); + xml.text(SyndDateUtils.formatRFC822Date(item.getPubDate())); + xml.endTag(null, "pubDate"); + } + if ((flags & FEATURE_WRITE_GUID) != 0) { + xml.startTag(null, "guid"); + xml.text(item.getItemIdentifier()); + xml.endTag(null, "guid"); + } + if (item.getMedia() != null) { + xml.startTag(null, "enclosure"); + xml.attribute(null, "url", item.getMedia().getDownload_url()); + xml.attribute(null, "length", String.valueOf(item.getMedia().getSize())); + xml.attribute(null, "type", item.getMedia().getMime_type()); + xml.endTag(null, "enclosure"); + } + if (item.getPaymentLink() != null) { + GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), true); + } + + xml.endTag(null, "item"); + } + } + + xml.endTag(null, "channel"); + xml.endTag(null, "rss"); + + xml.endDocument(); + } +} |