diff --git a/TODO.network b/TODO.network index 469ecac69..aef9fbf45 100644 --- a/TODO.network +++ b/TODO.network @@ -8,8 +8,8 @@ DONE сделать SeekBar (как в ветке alex) в пункте Navigate ** синхронизация ресурсов (последняя - 1604) ** Записать изменения в ChangeLog -* каталог должен иметь не 2 состояния (загружен/не загружен), а загружен полностью/не полностью/не загружен: - в случае загрузки "не полностью" при повторном открытии продолжать загрузку с точки, где прервались (для тех каталогов, что поделены на куски по 20 книг) +DONE каталог должен иметь не 2 состояния (загружен/не загружен), а загружен полностью/не полностью/не загружен: + в случае загрузки "не полностью" при повторном открытии продолжать загрузку с точки, где прервались (для тех каталогов, что поделены на куски по 20 книг) * 3 иконки в верхнем-правом углу для книжек DONE регистрация новых пользователей -- в диалоге authentication diff --git a/src/org/geometerplus/android/fbreader/network/ItemsLoadingRunnable.java b/src/org/geometerplus/android/fbreader/network/ItemsLoadingRunnable.java index 7493a8282..552fb11bf 100644 --- a/src/org/geometerplus/android/fbreader/network/ItemsLoadingRunnable.java +++ b/src/org/geometerplus/android/fbreader/network/ItemsLoadingRunnable.java @@ -30,8 +30,8 @@ abstract class ItemsLoadingRunnable implements Runnable { private final ItemsLoadingHandler myHandler; - private final long myUpdateInterval; // in milliseconds - private final int myItemsLimit; + public final long UpdateInterval; // in milliseconds + public final int ItemsLimit; private boolean myInterruptRequested; private boolean myInterruptConfirmed; @@ -42,13 +42,13 @@ abstract class ItemsLoadingRunnable implements Runnable { private Object myFinishedLock = new Object(); - public void interrupt() { + public void interruptLoading() { synchronized (myInterruptLock) { myInterruptRequested = true; } } - private boolean confirmInterrupt() { + private boolean confirmInterruptLoading() { synchronized (myInterruptLock) { if (myInterruptRequested) { myInterruptConfirmed = true; @@ -57,7 +57,7 @@ abstract class ItemsLoadingRunnable implements Runnable { } } - public boolean tryResume() { + public boolean tryResumeLoading() { synchronized (myInterruptLock) { if (!myInterruptConfirmed) { myInterruptRequested = false; @@ -66,12 +66,18 @@ abstract class ItemsLoadingRunnable implements Runnable { } } - private boolean isInterrupted() { + private boolean isLoadingInterrupted() { synchronized (myInterruptLock) { return myInterruptConfirmed; } } + private boolean isLoadingInterruptRequested() { + synchronized (myInterruptLock) { + return myInterruptRequested; + } + } + public ItemsLoadingRunnable(ItemsLoadingHandler handler) { this(handler, 1000, 500); @@ -79,8 +85,8 @@ abstract class ItemsLoadingRunnable implements Runnable { public ItemsLoadingRunnable(ItemsLoadingHandler handler, long updateIntervalMillis, int itemsLimit) { myHandler = handler; - myUpdateInterval = updateIntervalMillis; - myItemsLimit = itemsLimit; + UpdateInterval = updateIntervalMillis; + ItemsLimit = itemsLimit; } public abstract String doBefore(); @@ -98,24 +104,29 @@ abstract class ItemsLoadingRunnable implements Runnable { err = doLoading(new NetworkOperationData.OnNewItemListener() { private long myUpdateTime; private int myItemsNumber; - public boolean onNewItem(NetworkLibraryItem item) { + public void onNewItem(NetworkLibraryItem item) { myHandler.addItem(item); - final boolean interrupted = confirmInterrupt(); - if (interrupted || myItemsNumber++ >= myItemsLimit) { - return true; - } + ++myItemsNumber; final long now = System.currentTimeMillis(); if (now > myUpdateTime) { myHandler.sendUpdateItems(); - myUpdateTime = now + myUpdateInterval; + myUpdateTime = now + UpdateInterval; } - return false; + } + public boolean requestInterrupt() { + return isLoadingInterruptRequested() || myItemsNumber >= ItemsLimit; + } + public boolean confirmInterrupt() { + return confirmInterruptLoading() || myItemsNumber >= ItemsLimit; } }); myHandler.sendUpdateItems(); myHandler.ensureItemsProcessed(); - myHandler.sendFinish(err, isInterrupted()); + myHandler.sendFinish(err, isLoadingInterrupted()); myHandler.ensureFinishProcessed(); + } + + void runFinishHandler() { synchronized (myFinishedLock) { if (myFinishedHandler != null) { myFinishedHandler.sendEmptyMessage(0); @@ -124,6 +135,7 @@ abstract class ItemsLoadingRunnable implements Runnable { } } + public void runOnFinish(final Runnable runnable) { if (myFinishedHandler != null) { return; diff --git a/src/org/geometerplus/android/fbreader/network/NetworkCatalogActions.java b/src/org/geometerplus/android/fbreader/network/NetworkCatalogActions.java index 69a788764..d7db75b5a 100644 --- a/src/org/geometerplus/android/fbreader/network/NetworkCatalogActions.java +++ b/src/org/geometerplus/android/fbreader/network/NetworkCatalogActions.java @@ -288,7 +288,8 @@ class NetworkCatalogActions extends NetworkTreeActions { } public void onFinish(String errorMessage, boolean interrupted) { - if (interrupted) { + if (interrupted && + (!myTree.Item.supportsResumeLoading() || errorMessage != null)) { myTree.ChildrenItems.clear(); myTree.clear(); } else { @@ -336,11 +337,14 @@ class NetworkCatalogActions extends NetworkTreeActions { private final NetworkCatalogTree myTree; private final boolean myCheckAuthentication; + private final boolean myResumeNotLoad; - public ExpandCatalogRunnable(ItemsLoadingHandler handler, NetworkCatalogTree tree, boolean checkAuthentication) { + public ExpandCatalogRunnable(ItemsLoadingHandler handler, + NetworkCatalogTree tree, boolean checkAuthentication, boolean resumeNotLoad) { super(handler); myTree = tree; myCheckAuthentication = checkAuthentication; + myResumeNotLoad = resumeNotLoad; } public String getResourceKey() { @@ -369,6 +373,9 @@ class NetworkCatalogActions extends NetworkTreeActions { } public String doLoading(NetworkOperationData.OnNewItemListener doWithListener) { + if (myResumeNotLoad) { + return myTree.Item.resumeLoading(doWithListener); + } return myTree.Item.loadChildren(doWithListener); } } @@ -380,10 +387,15 @@ class NetworkCatalogActions extends NetworkTreeActions { } NetworkView.Instance().tryResumeLoading(activity, tree, url, new Runnable() { public void run() { + boolean resumeNotLoad = false; if (tree.hasChildren()) { if (tree.isContentValid()) { - NetworkView.Instance().openTree(activity, tree, url); - return; + if (tree.Item.supportsResumeLoading()) { + resumeNotLoad = true; + } else { + NetworkView.Instance().openTree(activity, tree, url); + return; + } } else { tree.ChildrenItems.clear(); tree.clear(); @@ -394,7 +406,7 @@ class NetworkCatalogActions extends NetworkTreeActions { NetworkView.Instance().startItemsLoading( activity, url, - new ExpandCatalogRunnable(handler, tree, true) + new ExpandCatalogRunnable(handler, tree, true, resumeNotLoad) ); NetworkView.Instance().openTree(activity, tree, url); } @@ -416,7 +428,7 @@ class NetworkCatalogActions extends NetworkTreeActions { NetworkView.Instance().startItemsLoading( activity, url, - new ExpandCatalogRunnable(handler, tree, false) + new ExpandCatalogRunnable(handler, tree, false, false) ); } diff --git a/src/org/geometerplus/android/fbreader/network/NetworkCatalogActivity.java b/src/org/geometerplus/android/fbreader/network/NetworkCatalogActivity.java index 0fc4b20cc..b2df97dba 100644 --- a/src/org/geometerplus/android/fbreader/network/NetworkCatalogActivity.java +++ b/src/org/geometerplus/android/fbreader/network/NetworkCatalogActivity.java @@ -216,7 +216,7 @@ public class NetworkCatalogActivity extends NetworkBaseActivity { if (key != null && NetworkView.Instance().isInitialized()) { final ItemsLoadingRunnable runnable = NetworkView.Instance().getItemsLoadingRunnable(key); if (runnable != null) { - runnable.interrupt(); + runnable.interruptLoading(); } } } diff --git a/src/org/geometerplus/android/fbreader/network/NetworkView.java b/src/org/geometerplus/android/fbreader/network/NetworkView.java index 94a5c1016..3242c63d9 100644 --- a/src/org/geometerplus/android/fbreader/network/NetworkView.java +++ b/src/org/geometerplus/android/fbreader/network/NetworkView.java @@ -153,9 +153,12 @@ class NetworkView { } } - ItemsLoadingRunnable removeItemsLoadingRunnable(String key) { + void removeItemsLoadingRunnable(String key) { synchronized (myItemsLoadingRunnables) { - return myItemsLoadingRunnables.remove(key); + ItemsLoadingRunnable runnable = myItemsLoadingRunnables.remove(key); + if (runnable != null) { + runnable.runFinishHandler(); + } } } @@ -164,8 +167,8 @@ class NetworkView { } public void tryResumeLoading(NetworkBaseActivity activity, NetworkTree tree, String key, Runnable expandRunnable) { - final ItemsLoadingRunnable runnable = NetworkView.Instance().getItemsLoadingRunnable(key); - if (runnable != null && runnable.tryResume()) { + final ItemsLoadingRunnable runnable = getItemsLoadingRunnable(key); + if (runnable != null && runnable.tryResumeLoading()) { openTree(activity, tree, key); return; } diff --git a/src/org/geometerplus/fbreader/network/AbstractNetworkLink.java b/src/org/geometerplus/fbreader/network/AbstractNetworkLink.java index 05ac8fb47..640e80e3f 100644 --- a/src/org/geometerplus/fbreader/network/AbstractNetworkLink.java +++ b/src/org/geometerplus/fbreader/network/AbstractNetworkLink.java @@ -73,4 +73,9 @@ public abstract class AbstractNetworkLink implements INetworkLink { public Set getLinkKeys() { return myLinks.keySet(); } + + public NetworkOperationData createOperationData(INetworkLink link, + NetworkOperationData.OnNewItemListener listener) { + return new NetworkOperationData(link, listener); + } } diff --git a/src/org/geometerplus/fbreader/network/INetworkLink.java b/src/org/geometerplus/fbreader/network/INetworkLink.java index 18a2354e9..a48d81b08 100644 --- a/src/org/geometerplus/fbreader/network/INetworkLink.java +++ b/src/org/geometerplus/fbreader/network/INetworkLink.java @@ -44,6 +44,9 @@ public interface INetworkLink { Set getLinkKeys(); + NetworkOperationData createOperationData(INetworkLink link, + NetworkOperationData.OnNewItemListener listener); + ZLNetworkRequest simpleSearchRequest(String pattern, NetworkOperationData data); ZLNetworkRequest resume(NetworkOperationData data); diff --git a/src/org/geometerplus/fbreader/network/NetworkCatalogItem.java b/src/org/geometerplus/fbreader/network/NetworkCatalogItem.java index 9f46beb6a..55228613c 100644 --- a/src/org/geometerplus/fbreader/network/NetworkCatalogItem.java +++ b/src/org/geometerplus/fbreader/network/NetworkCatalogItem.java @@ -92,13 +92,14 @@ public abstract class NetworkCatalogItem extends NetworkLibraryItem { public abstract String loadChildren(NetworkOperationData.OnNewItemListener listener); // returns Error Message - /*public final String loadChildren(final List children) { - return loadChildren(new CatalogListener() { - public void onNewItem(NetworkLibraryItem item) { - children.add(item); - } - }); - }*/ + public boolean supportsResumeLoading() { + return false; + } + + public String resumeLoading(NetworkOperationData.OnNewItemListener listener) { // returns Error Message + return NetworkErrors.errorMessage(NetworkErrors.ERROR_UNSUPPORTED_OPERATION); + } + /** * Method is called each time this item is displayed to the user. diff --git a/src/org/geometerplus/fbreader/network/NetworkLibrary.java b/src/org/geometerplus/fbreader/network/NetworkLibrary.java index 0b28df576..615cab884 100644 --- a/src/org/geometerplus/fbreader/network/NetworkLibrary.java +++ b/src/org/geometerplus/fbreader/network/NetworkLibrary.java @@ -410,9 +410,15 @@ public class NetworkLibrary { LinkedList requestList = new LinkedList(); LinkedList dataList = new LinkedList(); - NetworkOperationData.OnNewItemListener synchronizedListener = new NetworkOperationData.OnNewItemListener() { - public synchronized boolean onNewItem(NetworkLibraryItem item) { - return listener.onNewItem(item); + final NetworkOperationData.OnNewItemListener synchronizedListener = new NetworkOperationData.OnNewItemListener() { + public synchronized void onNewItem(NetworkLibraryItem item) { + listener.onNewItem(item); + } + public synchronized boolean requestInterrupt() { + return listener.requestInterrupt(); + } + public synchronized boolean confirmInterrupt() { + return listener.confirmInterrupt(); } }; @@ -421,8 +427,8 @@ public class NetworkLibrary { //if (link.OnOption.getValue()) { // execute next code only if link is enabled //} - NetworkOperationData data = new NetworkOperationData(link, synchronizedListener); - ZLNetworkRequest request = link.simpleSearchRequest(pattern, data); + final NetworkOperationData data = link.createOperationData(link, synchronizedListener); + final ZLNetworkRequest request = link.simpleSearchRequest(pattern, data); if (request != null) { dataList.add(data); requestList.add(request); diff --git a/src/org/geometerplus/fbreader/network/NetworkOperationData.java b/src/org/geometerplus/fbreader/network/NetworkOperationData.java index 1c2971a64..718bd25cd 100644 --- a/src/org/geometerplus/fbreader/network/NetworkOperationData.java +++ b/src/org/geometerplus/fbreader/network/NetworkOperationData.java @@ -25,12 +25,15 @@ import org.geometerplus.zlibrary.core.network.ZLNetworkRequest; public class NetworkOperationData { public interface OnNewItemListener { - // return true to interrupt reading; return false to continue reading - boolean onNewItem(NetworkLibraryItem item); + void onNewItem(NetworkLibraryItem item); + + // return true to confirm interrupt reading; return false to continue reading + boolean requestInterrupt(); + boolean confirmInterrupt(); } public final INetworkLink Link; - public final OnNewItemListener Listener; + public OnNewItemListener Listener; public String ResumeURI; private int myResumeCount; @@ -48,6 +51,8 @@ public class NetworkOperationData { if (++myResumeCount >= 10) { // FIXME: hardcoded resume limit constant!!! return null; } - return Link.resume(this); + final ZLNetworkRequest request = Link.resume(this); + clear(); + return request; } } diff --git a/src/org/geometerplus/fbreader/network/opds/NetworkOPDSFeedReader.java b/src/org/geometerplus/fbreader/network/opds/NetworkOPDSFeedReader.java index 459ecc12c..e643c9d87 100644 --- a/src/org/geometerplus/fbreader/network/opds/NetworkOPDSFeedReader.java +++ b/src/org/geometerplus/fbreader/network/opds/NetworkOPDSFeedReader.java @@ -31,10 +31,14 @@ import org.geometerplus.fbreader.network.authentication.litres.LitResBookshelfIt class NetworkOPDSFeedReader implements OPDSFeedReader { private final String myBaseURL; - private final NetworkOperationData myData; + private final OPDSCatalogItem.State myData; private int myIndex; + private String myNextURL; + private String mySkipUntilTitle; + + private int myItemsToLoad = -1; /** * Creates new OPDSFeedReader instance that can be used to get NetworkLibraryItem objects from OPDS feeds. @@ -43,15 +47,17 @@ class NetworkOPDSFeedReader implements OPDSFeedReader { * @param result network results buffer. Must be created using OPDSLink corresponding to the OPDS feed, * that will be read using this instance of the reader. */ - NetworkOPDSFeedReader(String baseURL, NetworkOperationData result) { + NetworkOPDSFeedReader(String baseURL, OPDSCatalogItem.State result) { myBaseURL = baseURL; myData = result; + mySkipUntilTitle = myData.LastLoadedTitle; if (!(result.Link instanceof OPDSLink)) { throw new IllegalArgumentException("Parameter `result` has invalid `Link` field value: result.Link must be an instance of OPDSLink class."); } } public void processFeedStart() { + myData.ResumeURI = myBaseURL; } private static String filter(String value) { @@ -64,6 +70,13 @@ class NetworkOPDSFeedReader implements OPDSFeedReader { public void processFeedMetadata(OPDSFeedMetadata feed, boolean beforeEntries) { if (beforeEntries) { myIndex = feed.OpensearchStartIndex - 1; + if (feed.OpensearchItemsPerPage > 0) { + myItemsToLoad = feed.OpensearchItemsPerPage; + final int len = feed.OpensearchTotalResults - myIndex; + if (len > 0 && len < myItemsToLoad) { + myItemsToLoad = len; + } + } return; } final OPDSLink opdsLink = (OPDSLink) myData.Link; @@ -72,12 +85,20 @@ class NetworkOPDSFeedReader implements OPDSFeedReader { String type = link.getType(); String rel = opdsLink.relation(filter(link.getRel()), type); if (type == OPDSConstants.MIME_APP_ATOM && rel == "next") { - myData.ResumeURI = href; + myNextURL = href; } } } public void processFeedEnd() { + if (mySkipUntilTitle != null) { + // Last loaded element was not found => resume error => DO NOT RESUME + // TODO: notify user about error??? + // TODO: do reload??? + myNextURL = null; + } + myData.ResumeURI = myNextURL; + myData.LastLoadedTitle = null; } @@ -110,10 +131,38 @@ class NetworkOPDSFeedReader implements OPDSFeedReader { } } + private boolean tryInterrupt() { + if (myData.Listener.requestInterrupt()) { + myData.Interrupt = true; + + final int noninterruptableRemainder = 10; + if (myItemsToLoad < 0 || myItemsToLoad > noninterruptableRemainder) { + if (myData.Listener.confirmInterrupt()) { + return true; + } else { + myData.Interrupt = false; + } + } + } + return false; + } + public boolean processFeedEntry(OPDSEntry entry) { + if (myItemsToLoad >= 0) { + --myItemsToLoad; + } + + if (mySkipUntilTitle != null) { + if (mySkipUntilTitle.equals(entry.Title)) { + mySkipUntilTitle = null; + } + return tryInterrupt(); + } + myData.LastLoadedTitle = entry.Title; + final OPDSLink opdsLink = (OPDSLink) myData.Link; if (opdsLink.getCondition(entry.Id.Uri) == OPDSLink.FeedCondition.NEVER) { - return false; + return tryInterrupt(); } boolean hasBookLink = false; for (ATOMLink link: entry.Links) { @@ -137,11 +186,9 @@ class NetworkOPDSFeedReader implements OPDSFeedReader { item = readCatalogItem(entry); } if (item != null) { - //item.dbgEntry = entry; - //myData.Items.add(item); - return myData.Listener.onNewItem(item); + myData.Listener.onNewItem(item); } - return false; + return tryInterrupt(); } private static final String AuthorPrefix = "author:"; diff --git a/src/org/geometerplus/fbreader/network/opds/OPDSCatalogItem.java b/src/org/geometerplus/fbreader/network/opds/OPDSCatalogItem.java index 6b62b63b7..98a011fee 100644 --- a/src/org/geometerplus/fbreader/network/opds/OPDSCatalogItem.java +++ b/src/org/geometerplus/fbreader/network/opds/OPDSCatalogItem.java @@ -29,6 +29,23 @@ import org.geometerplus.fbreader.network.*; class OPDSCatalogItem extends NetworkCatalogItem { + static class State extends NetworkOperationData { + + public String LastLoadedTitle; + public boolean Interrupt; + + public State(INetworkLink link, OnNewItemListener listener) { + super(link, listener); + } + + @Override + public void clear() { + super.clear(); + Interrupt = false; + } + } + private State myLoadingState; + OPDSCatalogItem(INetworkLink link, String title, String summary, String cover, Map urlByType) { super(link, title, summary, cover, urlByType); } @@ -41,21 +58,54 @@ class OPDSCatalogItem extends NetworkCatalogItem { super(link, title, summary, cover, urlByType, visibility, catalogType); } - @Override - public String loadChildren(NetworkOperationData.OnNewItemListener listener) { - - final NetworkOperationData data = new NetworkOperationData(Link, listener); - - ZLNetworkRequest networkRequest = - ((OPDSLink) Link).createNetworkData(URLByType.get(URL_CATALOG), data); - + private String doLoadChildren(NetworkOperationData.OnNewItemListener listener, + ZLNetworkRequest networkRequest) { while (networkRequest != null) { final String errorMessage = ZLNetworkManager.Instance().perform(networkRequest); if (errorMessage != null) { + myLoadingState = null; return errorMessage; } - networkRequest = data.resume(); + if (myLoadingState.Interrupt) { + if (myLoadingState.LastLoadedTitle == null) { + // In this case Catalog loading was not interrupted, and + // we must confirm interruption here. + if (listener.confirmInterrupt()) { + return null; + } + } else { + return null; + } + } + networkRequest = myLoadingState.resume(); } return null; } + + @Override + public final String loadChildren(NetworkOperationData.OnNewItemListener listener) { + OPDSLink opdsLink = (OPDSLink) Link; + + myLoadingState = opdsLink.createOperationData(Link, listener); + + ZLNetworkRequest networkRequest = + opdsLink.createNetworkData(URLByType.get(URL_CATALOG), myLoadingState); + + return doLoadChildren(listener, networkRequest); + } + + @Override + public final boolean supportsResumeLoading() { + return true; + } + + @Override + public final String resumeLoading(NetworkOperationData.OnNewItemListener listener) { + if (myLoadingState == null) { + return null; + } + myLoadingState.Listener = listener; + ZLNetworkRequest networkRequest = myLoadingState.resume(); + return doLoadChildren(listener, networkRequest); + } } diff --git a/src/org/geometerplus/fbreader/network/opds/OPDSLink.java b/src/org/geometerplus/fbreader/network/opds/OPDSLink.java index de596a35b..d56c30928 100644 --- a/src/org/geometerplus/fbreader/network/opds/OPDSLink.java +++ b/src/org/geometerplus/fbreader/network/opds/OPDSLink.java @@ -77,7 +77,7 @@ class OPDSLink extends AbstractNetworkLink { myAuthenticationManager = mgr; } - ZLNetworkRequest createNetworkData(String url, final NetworkOperationData result) { + ZLNetworkRequest createNetworkData(String url, final OPDSCatalogItem.State result) { if (url == null) { return null; } @@ -97,20 +97,24 @@ class OPDSLink extends AbstractNetworkLink { return getLink(URL_SEARCH).replace("%s", query); } + @Override + public OPDSCatalogItem.State createOperationData(INetworkLink link, + NetworkOperationData.OnNewItemListener listener) { + return new OPDSCatalogItem.State(link, listener); + } + public ZLNetworkRequest simpleSearchRequest(String pattern, NetworkOperationData data) { if (getLink(URL_SEARCH) == null) { return null; } return createNetworkData( searchURL(ZLNetworkUtil.htmlEncode(pattern)), - data + (OPDSCatalogItem.State) data ); } public ZLNetworkRequest resume(NetworkOperationData data) { - String url = data.ResumeURI; - data.clear(); - return createNetworkData(url, data); + return createNetworkData(data.ResumeURI, (OPDSCatalogItem.State) data); } public NetworkLibraryItem libraryItem() { diff --git a/src/org/geometerplus/fbreader/network/opds/OPDSXMLReader.java b/src/org/geometerplus/fbreader/network/opds/OPDSXMLReader.java index 6cc4b27c8..e993b7cad 100644 --- a/src/org/geometerplus/fbreader/network/opds/OPDSXMLReader.java +++ b/src/org/geometerplus/fbreader/network/opds/OPDSXMLReader.java @@ -173,6 +173,7 @@ class OPDSXMLReader extends ZLXMLReaderAdapter { private final StringBuffer myBuffer = new StringBuffer(); private HtmlToString myHtmlToString = new HtmlToString(); + private boolean myFeedMetadataProcessed; @Override public boolean startElementHandler(String tag, ZLStringMap attributes) { @@ -202,6 +203,7 @@ class OPDSXMLReader extends ZLXMLReaderAdapter { myFeed = new OPDSFeedMetadata(); myFeed.readAttributes(attributes); myState = FEED; + myFeedMetadataProcessed = false; } break; case FEED: @@ -236,8 +238,9 @@ class OPDSXMLReader extends ZLXMLReaderAdapter { myEntry.readAttributes(attributes); myState = F_ENTRY; // Process feed metadata just before first feed entry - if (myFeed != null && myFeed.Id != null) { + if (myFeed != null && myFeed.Id != null && !myFeedMetadataProcessed) { myFeedReader.processFeedMetadata(myFeed, true); + myFeedMetadataProcessed = true; } } } else if (tagPrefix == myOpenSearchNamespaceId) {