Avoid loading Chapters of multiple FeedItems at the same time

This should significantly reduce the time needed to load FeedItem lists with chapters, because chapters are from now on only loaded when a single FeedItem is requested.
This commit is contained in:
daniel oeh 2014-12-07 21:24:03 +01:00
parent 24538d7ebb
commit f534481ed0
7 changed files with 271 additions and 98 deletions

View File

@ -2,6 +2,12 @@ package de.test.antennapod.storage;
import android.content.Context;
import android.test.InstrumentationTestCase;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@ -10,11 +16,6 @@ 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;
/**
@ -325,7 +326,7 @@ public class DBReaderTest extends InstrumentationTestCase {
public void testGetPlaybackHistory() {
final Context context = getInstrumentation().getTargetContext();
final int numItems = (DBReader.PLAYBACK_HISTORY_SIZE+1) * 2;
final int numItems = (DBReader.PLAYBACK_HISTORY_SIZE + 1) * 2;
final int playedItems = DBReader.PLAYBACK_HISTORY_SIZE + 1;
final int numReturnedItems = Math.min(playedItems, DBReader.PLAYBACK_HISTORY_SIZE);
final int numFeeds = 1;
@ -405,4 +406,64 @@ public class DBReaderTest extends InstrumentationTestCase {
assertEquals(NUM_UNREAD, navDrawerData.numUnreadItems);
assertEquals(NUM_QUEUE, navDrawerData.queueSize);
}
public void testGetFeedItemlistCheckChaptersFalse() throws Exception {
Context context = getInstrumentation().getTargetContext();
List<Feed> feeds = DBTestUtils.saveFeedlist(context, 10, 10, false, false, 0);
for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) {
assertFalse(item.hasChapters());
}
}
}
public void testGetFeedItemlistCheckChaptersTrue() throws Exception {
Context context = getInstrumentation().getTargetContext();
List<Feed> feeds = saveFeedlist(context, 10, 10, false, true, 10);
for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) {
assertTrue(item.hasChapters());
}
}
}
public void testLoadChaptersOfFeedItemNoChapters() throws Exception {
Context context = getInstrumentation().getTargetContext();
List<Feed> feeds = saveFeedlist(context, 1, 3, false, false, 0);
saveFeedlist(context, 1, 3, false, true, 3);
for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) {
assertFalse(item.hasChapters());
DBReader.loadChaptersOfFeedItem(context, item);
assertFalse(item.hasChapters());
assertNull(item.getChapters());
}
}
}
public void testLoadChaptersOfFeedItemWithChapters() throws Exception {
Context context = getInstrumentation().getTargetContext();
final int NUM_CHAPTERS = 3;
DBTestUtils.saveFeedlist(context, 1, 3, false, false, 0);
List<Feed> feeds = saveFeedlist(context, 1, 3, false, true, NUM_CHAPTERS);
for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) {
assertTrue(item.hasChapters());
DBReader.loadChaptersOfFeedItem(context, item);
assertTrue(item.hasChapters());
assertNotNull(item.getChapters());
assertEquals(NUM_CHAPTERS, item.getChapters().size());
}
}
}
public void testGetItemWithChapters() throws Exception {
Context context = getInstrumentation().getTargetContext();
final int NUM_CHAPTERS = 3;
List<Feed> feeds = saveFeedlist(context, 1, 1, false, true, NUM_CHAPTERS);
FeedItem item1 = feeds.get(0).getItems().get(0);
FeedItem item2 = DBReader.getFeedItem(context, item1.getId());
assertTrue(item2.hasChapters());
assertEquals(item1.getChapters(), item2.getChapters());
}
}

View File

@ -1,12 +1,7 @@
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;
@ -14,12 +9,32 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.SimpleChapter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
/**
* Utility methods for DB* tests.
*/
public class DBTestUtils {
/**
* Use this method when tests don't involve chapters.
*/
public static List<Feed> saveFeedlist(Context context, int numFeeds, int numItems, boolean withMedia) {
return saveFeedlist(context, numFeeds, numItems, withMedia, false, 0);
}
/**
* Use this method when tests involve chapters.
*/
public static List<Feed> saveFeedlist(Context context, int numFeeds, int numItems, boolean withMedia,
boolean withChapters, int numChapters) {
if (numFeeds <= 0) {
throw new IllegalArgumentException("numFeeds<=0");
}
@ -36,11 +51,18 @@ public class DBTestUtils {
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);
true, f, withChapters);
if (withMedia) {
FeedMedia media = new FeedMedia(item, "url" + j, 1, "audio/mp3");
item.setMedia(media);
}
if (withChapters) {
List<Chapter> chapters = new ArrayList<>();
item.setChapters(chapters);
for (int k = 0; k < numChapters; k++) {
chapters.add(new SimpleChapter(k, "item " + j + " chapter " + k, item, "http://example.com"));
}
}
f.getItems().add(item);
}
Collections.sort(f.getItems(), new FeedItemPubdateComparator());
@ -52,6 +74,7 @@ public class DBTestUtils {
feeds.add(f);
}
adapter.close();
return feeds;
}
}

View File

@ -4,10 +4,13 @@ import android.content.Context;
import android.database.Cursor;
import android.test.InstrumentationTestCase;
import android.util.Log;
import de.danoeh.antennapod.core.feed.Chapter;
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.feed.SimpleChapter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
@ -101,7 +104,7 @@ public class DBWriterTest extends InstrumentationTestCase {
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);
FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed, true);
feed.getItems().add(item);
File enc = new File(destFolder, "file " + i);
@ -110,6 +113,9 @@ public class DBWriterTest extends InstrumentationTestCase {
FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0);
item.setMedia(media);
item.setChapters(new ArrayList<Chapter>());
item.getChapters().add(new SimpleChapter(0, "item " + i, item, "example.com"));
}
PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
@ -122,6 +128,7 @@ public class DBWriterTest extends InstrumentationTestCase {
for (FeedItem item : feed.getItems()) {
assertTrue(item.getId() != 0);
assertTrue(item.getMedia().getId() != 0);
assertTrue(item.getChapters().get(0).getId() != 0);
}
DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
@ -135,18 +142,20 @@ public class DBWriterTest extends InstrumentationTestCase {
adapter = new PodDBAdapter(getInstrumentation().getContext());
adapter.open();
Cursor c = adapter.getFeedCursor(feed.getId());
assertTrue(c.getCount() == 0);
assertEquals(0, c.getCount());
c.close();
c = adapter.getImageCursor(image.getId());
assertTrue(c.getCount() == 0);
assertEquals(0, c.getCount());
c.close();
for (FeedItem item : feed.getItems()) {
c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
assertTrue(c.getCount() == 0);
assertEquals(0, c.getCount());
c.close();
c = adapter.getSingleFeedMediaCursor(item.getMedia().getId());
assertTrue(c.getCount() == 0);
assertEquals(0, c.getCount());
c.close();
c = adapter.getSimpleChaptersOfFeedItemCursor(item);
assertEquals(0, c.getCount());
}
}

View File

@ -44,12 +44,45 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
private boolean read;
private String paymentLink;
private FlattrStatus flattrStatus;
/**
* Is true if the database contains any chapters that belong to this item. This attribute is only
* written once by DBReader on initialization.
* The FeedItem might still have a non-null chapters value. In this case, the list of chapters
* has not been saved in the database yet.
* */
private final boolean hasChapters;
/**
* The list of chapters of this item. This might be null even if there are chapters of this item
* in the database. The 'hasChapters' attribute should be used to check if this item has any chapters.
* */
private List<Chapter> chapters;
private FeedImage image;
public FeedItem() {
this.read = true;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
}
/**
* This constructor is used by DBReader.
* */
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, boolean read,
String itemIdentifier) {
this.id = id;
this.title = title;
this.link = link;
this.pubDate = pubDate;
this.paymentLink = paymentLink;
this.feedId = feedId;
this.flattrStatus = flattrStatus;
this.hasChapters = hasChapters;
this.image = image;
this.read = read;
this.itemIdentifier = itemIdentifier;
}
/**
@ -64,6 +97,22 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
this.read = read;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
}
/**
* This constructor should be used for creating test objects involving chapter marks.
*/
public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed, boolean hasChapters) {
this.id = id;
this.title = title;
this.itemIdentifier = itemIdentifier;
this.link = link;
this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
this.read = read;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = hasChapters;
}
public void updateFromOther(FeedItem other) {
@ -331,4 +380,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
public String getHumanReadableIdentifier() {
return title;
}
public boolean hasChapters() {
return hasChapters;
}
}

View File

@ -245,14 +245,19 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public void loadChapterMarks() {
if (getChapters() == null && !localFileAvailable()) {
if (item == null && itemID != 0) {
item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
}
// check if chapters are stored in db and not loaded yet.
if (item != null && item.hasChapters() && item.getChapters() == null) {
DBReader.loadChaptersOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), item);
} else if (item != null && item.getChapters() == null && !localFileAvailable()) {
ChapterUtils.loadChaptersFromStreamUrl(this);
if (getChapters() != null && item != null) {
DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(),
item);
}
}
}
@Override

View File

@ -4,8 +4,22 @@ import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.*;
import de.danoeh.antennapod.core.feed.Chapter;
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.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.ID3Chapter;
import de.danoeh.antennapod.core.feed.SimpleChapter;
import de.danoeh.antennapod.core.feed.VorbisCommentChapter;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
@ -14,11 +28,6 @@ import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparato
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Provides methods for reading data from the AntennaPod database.
* In general, all database calls in DBReader-methods are executed on the caller's thread.
@ -203,75 +212,26 @@ public final class DBReader {
if (itemlistCursor.moveToFirst()) {
do {
FeedItem item = new FeedItem();
long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE);
FeedImage image = null;
if (imageIndex != 0) {
image = getFeedImage(adapter, imageIndex);
}
FeedItem item = new FeedItem(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID),
itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_TITLE),
itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_LINK),
new Date(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)),
itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK),
itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FEED),
new FlattrStatus(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)),
itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0,
image,
(itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0),
itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER));
item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID));
item.setTitle(itemlistCursor
.getString(PodDBAdapter.IDX_FI_SMALL_TITLE));
item.setLink(itemlistCursor
.getString(PodDBAdapter.IDX_FI_SMALL_LINK));
item.setPubDate(new Date(itemlistCursor
.getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)));
item.setPaymentLink(itemlistCursor
.getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK));
item.setFeedId(itemlistCursor
.getLong(PodDBAdapter.IDX_FI_SMALL_FEED));
itemIds.add(String.valueOf(item.getId()));
item.setRead((itemlistCursor
.getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0));
item.setItemIdentifier(itemlistCursor
.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER));
item.setFlattrStatus(new FlattrStatus(itemlistCursor
.getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)));
long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE);
if (imageIndex != 0) {
item.setImage(getFeedImage(adapter, imageIndex));
}
// extract chapters
boolean hasSimpleChapters = itemlistCursor
.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0;
if (hasSimpleChapters) {
Cursor chapterCursor = adapter
.getSimpleChaptersOfFeedItemCursor(item);
if (chapterCursor.moveToFirst()) {
item.setChapters(new ArrayList<Chapter>());
do {
int chapterType = chapterCursor
.getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX);
Chapter chapter = null;
long start = chapterCursor
.getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX);
String title = chapterCursor
.getString(PodDBAdapter.KEY_TITLE_INDEX);
String link = chapterCursor
.getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX);
switch (chapterType) {
case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
chapter = new SimpleChapter(start, title, item,
link);
break;
case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
chapter = new ID3Chapter(start, title, item,
link);
break;
case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
chapter = new VorbisCommentChapter(start,
title, item, link);
break;
}
if (chapter != null) {
chapter.setId(chapterCursor
.getLong(PodDBAdapter.KEY_ID_INDEX));
item.getChapters().add(chapter);
}
} while (chapterCursor.moveToNext());
}
chapterCursor.close();
}
items.add(item);
} while (itemlistCursor.moveToNext());
}
@ -367,6 +327,7 @@ public final class DBReader {
return feed;
}
private static FeedItem getMatchingItemForMedia(long itemId,
List<FeedItem> items) {
for (FeedItem item : items) {
@ -689,6 +650,9 @@ public final class DBReader {
if (list.size() > 0) {
item = list.get(0);
loadFeedDataOfFeedItemlist(context, list);
if (item.hasChapters()) {
loadChaptersOfFeedItem(adapter, item);
}
}
}
return item;
@ -696,12 +660,13 @@ public final class DBReader {
}
/**
* Loads a specific FeedItem from the database.
* Loads a specific FeedItem from the database. This method should not be used for loading more
* than one FeedItem because this method might query the database several times for each item.
*
* @param context A context that is used for opening a database connection.
* @param itemId The ID of the FeedItem
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will
* also be loaded from the database.
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes
* as well as chapter marks of the FeedItem will also be loaded from the database.
*/
public static FeedItem getFeedItem(final Context context, final long itemId) {
if (BuildConfig.DEBUG)
@ -736,6 +701,63 @@ public final class DBReader {
adapter.close();
}
/**
* Loads the list of chapters that belongs to this FeedItem if available. This method overwrites
* any chapters that this FeedItem has. If no chapters were found in the database, the chapters
* reference of the FeedItem will be set to null.
*
* @param context A context that is used for opening a database connection.
* @param item The FeedItem
*/
public static void loadChaptersOfFeedItem(final Context context, final FeedItem item) {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
loadChaptersOfFeedItem(adapter, item);
adapter.close();
}
static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
Cursor chapterCursor = adapter
.getSimpleChaptersOfFeedItemCursor(item);
if (chapterCursor.moveToFirst()) {
item.setChapters(new ArrayList<Chapter>());
do {
int chapterType = chapterCursor
.getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX);
Chapter chapter = null;
long start = chapterCursor
.getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX);
String title = chapterCursor
.getString(PodDBAdapter.KEY_TITLE_INDEX);
String link = chapterCursor
.getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX);
switch (chapterType) {
case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
chapter = new SimpleChapter(start, title, item,
link);
break;
case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
chapter = new ID3Chapter(start, title, item,
link);
break;
case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
chapter = new VorbisCommentChapter(start,
title, item, link);
break;
}
if (chapter != null) {
chapter.setId(chapterCursor
.getLong(PodDBAdapter.KEY_ID_INDEX));
item.getChapters().add(chapter);
}
} while (chapterCursor.moveToNext());
} else {
item.setChapters(null);
}
chapterCursor.close();
}
/**
* Returns the number of downloaded episodes.
*
@ -788,7 +810,7 @@ public final class DBReader {
static FeedImage getFeedImage(PodDBAdapter adapter, final long id) {
Cursor cursor = adapter.getImageCursor(id);
if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
throw new SQLException("No FeedImage found at index: " + id);
return null;
}
FeedImage image = new FeedImage(id, cursor.getString(cursor
.getColumnIndex(PodDBAdapter.KEY_TITLE)),

View File

@ -693,7 +693,7 @@ public class PodDBAdapter {
}
values.put(KEY_FEED, item.getFeed().getId());
values.put(KEY_READ, item.isRead());
values.put(KEY_HAS_CHAPTERS, item.getChapters() != null);
values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters());
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong());
if (item.hasItemImage()) {
@ -848,7 +848,7 @@ public class PodDBAdapter {
if (item.getMedia() != null) {
removeFeedMedia(item.getMedia());
}
if (item.getChapters() != null) {
if (item.hasChapters() || item.getChapters() != null) {
removeChaptersOfItem(item);
}
if (item.hasItemImage()) {