engine/src/FeedEngine.cpp
changeset 2 29cda98b007e
child 7 a7a43293ae56
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/src/FeedEngine.cpp	Thu Feb 25 14:29:19 2010 +0000
@@ -0,0 +1,1077 @@
+/*
+* Copyright (c) 2007-2010 Sebastian Brannstrom, Lars Persson, EmbedDev AB
+*
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* EmbedDev AB - initial contribution.
+*
+* Contributors:
+*
+* Description:
+*
+*/
+
+#include "FeedEngine.h"
+#include <f32file.h>
+#include <bautils.h>
+#include <s32file.h>
+#include "SettingsEngine.h"
+#include "ShowEngine.h"
+#include <e32hashtab.h>
+#include "OpmlParser.h"
+#include "PodcastUtils.h"
+#include <utf.h>
+
+// Cleanup stack macro for SQLite3
+// TODO Move this to some common place.
+static void Cleanup_sqlite3_finalize_wrapper(TAny* handle)
+	{
+	sqlite3_finalize(static_cast<sqlite3_stmt*>(handle));
+	}
+#define Cleanup_sqlite3_finalize_PushL(__handle) CleanupStack::PushL(TCleanupItem(&Cleanup_sqlite3_finalize_wrapper, __handle))
+
+
+CFeedEngine* CFeedEngine::NewL(CPodcastModel& aPodcastModel)
+	{
+	CFeedEngine* self = new (ELeave) CFeedEngine(aPodcastModel);
+	CleanupStack::PushL(self);
+	self->ConstructL();
+	CleanupStack::Pop(self);
+	return self;
+	}
+
+void CFeedEngine::ConstructL()
+	{
+	iParser = new (ELeave) CFeedParser(*this, iPodcastModel.FsSession());
+	
+	iFeedClient = CHttpClient::NewL(iPodcastModel, *this);
+	iFeedTimer.ConstructL();
+	
+	RunFeedTimer();
+	
+    if (DBGetFeedCount() > 0) 
+    	{
+		DP("Loading feeds from DB");
+		DBLoadFeedsL();
+		} 
+    
+	if (iPodcastModel.IsFirstStartup()) {
+		TFileName defaultFile = iPodcastModel.SettingsEngine().DefaultFeedsFileName();
+		DP1("Loading default feeds from %S", &defaultFile);
+		if (BaflUtils::FileExists(iPodcastModel.FsSession(), defaultFile)) {
+			ImportFeedsL(defaultFile);
+		}
+	}
+    
+    TFileName importFile = iPodcastModel.SettingsEngine().ImportFeedsFileName();
+    if (BaflUtils::FileExists(iPodcastModel.FsSession(), importFile)) {
+    	DP("Importing feeds");
+    	ImportFeedsL(importFile);
+		}
+	}
+
+CFeedEngine::CFeedEngine(CPodcastModel& aPodcastModel)
+		: iClientState(EIdle),
+		  iFeedTimer(this),
+		  iPodcastModel(aPodcastModel),
+		  iDB(*aPodcastModel.DB())
+	{
+	}
+
+CFeedEngine::~CFeedEngine()
+	{
+	iObservers.Close();
+	
+	iFeedsUpdating.Close();
+	iSortedFeeds.ResetAndDestroy();
+	iSearchResults.ResetAndDestroy();
+	
+	delete iParser;
+	delete iFeedClient;
+	delete iOpmlParser;
+	}
+
+/**
+ * Returns the current internal state of the feed engine4
+ */
+EXPORT_C TClientState CFeedEngine::ClientState()
+	{
+	return iClientState;
+	}
+
+
+/**
+ * Returns the current updating client UID if clientstate is != ENotUpdateing
+ * @return TUint
+ */
+EXPORT_C TUint CFeedEngine::ActiveClientUid()
+	{
+	if(iActiveFeed != NULL)
+		{
+		return iActiveFeed->Uid();
+		}
+	return 0;	
+	}
+
+void CFeedEngine::RunFeedTimer()
+	{
+	iFeedTimer.Cancel();
+
+	if (iPodcastModel.SettingsEngine().UpdateAutomatically() != EAutoUpdateOff)
+		{
+		TInt interval = iPodcastModel.SettingsEngine().UpdateFeedInterval();
+	
+		if (interval != 0)
+			{
+			iFeedTimer.SetPeriod(interval);
+			iFeedTimer.RunPeriodically();
+			}
+		}
+	}
+
+EXPORT_C void CFeedEngine::UpdateAllFeedsL(TBool aAutoUpdate)
+	{
+	iAutoUpdatedInitiator = aAutoUpdate;
+	if (iFeedsUpdating.Count() > 0)
+		{
+		DP("Cancelling update");
+		iFeedClient->Stop();
+		iFeedsUpdating.Reset();
+		return;
+		}
+
+	TInt cnt = iSortedFeeds.Count();
+	for (int i=0;i<cnt;i++)
+		{
+		iFeedsUpdating.Append(iSortedFeeds[i]);
+		}
+
+	UpdateNextFeedL();
+	}
+
+EXPORT_C void CFeedEngine::CancelUpdateAllFeeds()
+	{
+	if(iClientState != EIdle)
+		{
+		iFeedsUpdating.Reset();
+		iFeedClient->Stop();
+		}
+	}
+
+void CFeedEngine::UpdateNextFeedL()
+	{
+	DP1("UpdateNextFeed. %d feeds left to update", iFeedsUpdating.Count());
+	
+	if (iFeedsUpdating.Count() > 0)
+		{
+		CFeedInfo *info = iFeedsUpdating[0];
+		iFeedsUpdating.Remove(0);
+		TBool result = EFalse;
+		//DP2("** UpdateNextFeed: %S, ID: %u", &(info->Url()), info->Uid());
+		TRAPD(error, result = UpdateFeedL(info->Uid()));
+		
+		if (error != KErrNone || !result)
+			{
+			DP("Error while updating all feeds");
+			for (TInt i=0;i<iObservers.Count();i++) 
+				{
+				TRAP_IGNORE(iObservers[i]->FeedUpdateAllCompleteL(iAutoUpdatedInitiator?MFeedEngineObserver::EFeedAutoUpdate:MFeedEngineObserver::EFeedManualUpdate));
+				}
+			}
+		}
+	else
+		{
+		iClientState = EIdle;
+		for (TInt i=0;i<iObservers.Count();i++) 
+			{
+			TRAP_IGNORE(iObservers[i]->FeedUpdateAllCompleteL(iAutoUpdatedInitiator?MFeedEngineObserver::EFeedAutoUpdate:MFeedEngineObserver::EFeedManualUpdate));
+			}
+		}
+	}
+
+EXPORT_C TBool CFeedEngine::UpdateFeedL(TUint aFeedUid)
+	{
+	DP("FeedEngine::UpdateFeedL BEGIN");
+	iActiveFeed = GetFeedInfoByUid(aFeedUid);
+	iCatchupCounter = 0;
+
+	if (iActiveFeed->LastUpdated() == 0)
+		{
+		iCatchupMode = ETrue;
+		}
+
+	iUpdatingFeedFileName.Copy (iPodcastModel.SettingsEngine().PrivatePath ());
+	_LIT(KFileNameFormat, "%lu.xml");
+	iUpdatingFeedFileName.AppendFormat(KFileNameFormat, aFeedUid);
+	
+	if(iFeedClient->GetL(iActiveFeed->Url(), iUpdatingFeedFileName, iPodcastModel.SettingsEngine().SpecificIAP()))
+		{
+		iClientState = EUpdatingFeed;
+		
+		for (TInt i=0;i<iObservers.Count();i++)
+			{
+			TRAP_IGNORE(iObservers[i]->FeedDownloadStartedL(iAutoUpdatedInitiator?MFeedEngineObserver::EFeedAutoUpdate:MFeedEngineObserver::EFeedManualUpdate, iActiveFeed->Uid()));
+			}
+
+		DP("FeedEngine::UpdateFeedL END, return ETrue");
+		return ETrue;
+		}
+	else
+		{
+		DP("FeedEngine::UpdateFeedL END, return EFalse");
+		return EFalse;
+		}
+	}
+
+void CFeedEngine::NewShowL(CShowInfo& aItem)
+	{
+	//DP4("\nTitle: %S\nURL: %S\nDescription length: %d\nFeed: %d", &(item->Title()), &(item->Url()), item->Description().Length(), item->FeedUid());
+	
+	HBufC* description = HBufC::NewLC(KMaxDescriptionLength);
+	TPtr ptr(description->Des());
+	ptr.Copy(aItem.Description());
+	PodcastUtils::CleanHtmlL(ptr);
+	//DP1("New show has feed ID: %u") item->FeedUid());
+	TRAP_IGNORE(aItem.SetDescriptionL(*description));
+	CleanupStack::PopAndDestroy(description);
+	//DP1("Description: %S", &description);
+
+	if (iCatchupMode) {
+		// in catchup mode, we let one show be unplayed
+		if (++iCatchupCounter > 1) {
+			aItem.SetPlayState(EPlayed);
+		}
+	}
+	
+	TBool isShowAdded = iPodcastModel.ShowEngine().AddShowL(aItem);
+
+	if (aItem.PlayState() == ENeverPlayed && isShowAdded && iPodcastModel.SettingsEngine().DownloadAutomatically()) 
+		{
+		iPodcastModel.ShowEngine().AddDownloadL(aItem);
+		}	
+	}
+
+void CFeedEngine::GetFeedImageL(CFeedInfo *aFeedInfo)
+	{
+	DP("GetFeedImage");
+
+	TFileName filePath;
+	filePath.Copy(iPodcastModel.SettingsEngine().BaseDir());
+	
+	// create relative file name
+	TFileName relPath;
+	relPath.Copy(aFeedInfo->Title());
+	relPath.Append('\\');
+
+	TFileName fileName;
+	PodcastUtils::FileNameFromUrl(aFeedInfo->ImageUrl(), fileName);
+	relPath.Append(fileName);
+	PodcastUtils::EnsureProperPathName(relPath);
+	
+	// complete file path is base dir + rel path
+	filePath.Append(relPath);
+	aFeedInfo->SetImageFileNameL(filePath);
+
+	if(iFeedClient->GetL(aFeedInfo->ImageUrl(), filePath, ETrue))
+		{
+			iClientState = EUpdatingImage;
+		}
+	}
+
+EXPORT_C TBool CFeedEngine::AddFeedL(const CFeedInfo&aItem) 
+	{
+	DP2("CFeedEngine::AddFeed, title=%S, URL=%S", &aItem.Title(), &aItem.Url());
+	for (TInt i=0;i<iSortedFeeds.Count();i++) 
+		{
+		if (iSortedFeeds[i]->Uid() == aItem.Uid()) 
+			{
+			DP1("Already have feed %S, discarding", &aItem.Url());			
+			return EFalse;
+			}
+		}
+	
+	TLinearOrder<CFeedInfo> sortOrder( CFeedEngine::CompareFeedsByTitle);
+	CFeedInfo* newItem = aItem.CopyL();
+	CleanupStack::PushL(newItem);
+	User::LeaveIfError(iSortedFeeds.InsertInOrder(newItem, sortOrder));
+	CleanupStack::Pop(newItem);
+	
+
+	// Save the feeds into DB
+	DBAddFeedL(aItem);
+	return ETrue;
+	}
+
+TBool CFeedEngine::DBAddFeedL(const CFeedInfo& aItem)
+	{
+	DP2("CFeedEngine::DBAddFeed, title=%S, URL=%S", &aItem.Title(), &aItem.Url());
+	
+	CFeedInfo *info = DBGetFeedInfoByUidL(aItem.Uid());
+	if (info) {
+		DP("Feed already exists!");
+		delete info;
+		return EFalse;
+	}
+
+	HBufC* titleBuf = HBufC::NewLC(KMaxLineLength);
+	TPtr titlePtr(titleBuf->Des());
+	titlePtr.Copy(aItem.Title());
+	PodcastUtils::SQLEncode(titlePtr);
+	
+	HBufC* descBuf = HBufC::NewLC(KMaxLineLength);
+	TPtr descPtr(descBuf->Des());
+	descPtr.Copy(aItem.Description());
+	PodcastUtils::SQLEncode(descPtr);
+	
+	_LIT(KSqlStatement, "insert into feeds (url, title, description, imageurl, imagefile, link, built, lastupdated, uid, feedtype, customtitle, lasterror)"
+			" values (\"%S\",\"%S\", \"%S\", \"%S\", \"%S\", \"%S\", \"%Ld\", \"%Ld\", \"%u\", \"%u\", \"%u\", \"%d\")");
+	iSqlBuffer.Format(KSqlStatement,
+			&aItem.Url(), titleBuf, descBuf, &aItem.ImageUrl(), &aItem.ImageFileName(), &aItem.Link(),
+			aItem.BuildDate().Int64(), aItem.LastUpdated().Int64(), aItem.Uid(), EAudioPodcast, aItem.CustomTitle(), aItem.LastError());
+
+	CleanupStack::PopAndDestroy(descBuf);
+	CleanupStack::PopAndDestroy(titleBuf);
+	
+	sqlite3_stmt *st;
+	 
+	//DP1("SQL statement length=%d", iSqlBuffer.Length());
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st, (const void**) NULL);
+	
+	if (rc==SQLITE_OK)
+		{
+		rc = sqlite3_step(st);
+
+		if (rc == SQLITE_DONE)
+			{
+			sqlite3_finalize(st);
+			return ETrue;
+			}
+		else {
+			sqlite3_finalize(st);
+		}
+	}
+
+	return EFalse;
+	}
+
+EXPORT_C void CFeedEngine::RemoveFeedL(TUint aUid) 
+	{
+	for (int i=0;i<iSortedFeeds.Count();i++) 
+		{
+		if (iSortedFeeds[i]->Uid() == aUid) 
+			{
+			iPodcastModel.ShowEngine().DeleteAllShowsByFeedL(aUid);
+					
+			CFeedInfo* feedToRemove = iSortedFeeds[i];
+			
+			//delete the image file if it exists
+			if ((feedToRemove->ImageFileName().Length() >0)
+					&& BaflUtils::FileExists(iPodcastModel.FsSession(), feedToRemove->ImageFileName()))
+				{
+				iPodcastModel.FsSession().Delete(feedToRemove->ImageFileName());
+				}
+				
+			//delete the folder. It has the same name as the title.
+			TFileName filePath;
+			filePath.Copy(iPodcastModel.SettingsEngine().BaseDir());
+			filePath.Append(feedToRemove->Title());
+			filePath.Append('\\');
+			iPodcastModel.FsSession().RmDir(filePath);
+
+			iSortedFeeds.Remove(i);
+			delete feedToRemove;
+			
+			DP("Removed feed from array");
+			
+			// now remove it from DB
+			DBRemoveFeed(aUid);
+
+			return;
+		}
+	}
+}
+
+
+TBool CFeedEngine::DBRemoveFeed(TUint aUid)
+	{
+	DP("CFeedEngine::DBRemoveFeed");
+	_LIT(KSqlStatement, "delete from feeds where uid=%u");
+	iSqlBuffer.Format(KSqlStatement, aUid);
+
+	sqlite3_stmt *st;
+	 
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+	
+	if (rc==SQLITE_OK)
+		{
+		rc = sqlite3_step(st);
+		sqlite3_finalize(st);
+
+		if (rc == SQLITE_DONE)
+			{
+			DP("Feed removed from DB");
+			return ETrue;
+			}
+		else
+			{
+			DP("Error when removing feed from DB");
+			}
+		}
+	return EFalse;	
+	}
+
+TBool CFeedEngine::DBUpdateFeed(const CFeedInfo &aItem)
+	{
+	DP2("CFeedEngine::DBUpdateFeed, title=%S, URL=%S", &aItem.Title(), &aItem.Url());
+	
+	HBufC* titleBuf = HBufC::NewLC(KMaxLineLength);
+	TPtr titlePtr(titleBuf->Des());
+	titlePtr.Copy(aItem.Title());
+	PodcastUtils::SQLEncode(titlePtr);
+	
+	HBufC* descBuf = HBufC::NewLC(KMaxLineLength);
+	TPtr descPtr(descBuf->Des());
+	descPtr.Copy(aItem.Description());
+	PodcastUtils::SQLEncode(descPtr);
+	
+	_LIT(KSqlStatement, "update feeds set url=\"%S\", title=\"%S\", description=\"%S\", imageurl=\"%S\", imagefile=\"%S\"," \
+			"link=\"%S\", built=\"%Lu\", lastupdated=\"%Lu\", feedtype=\"%u\", customtitle=\"%u\", lasterror=\"%d\" where uid=\"%u\"");
+	iSqlBuffer.Format(KSqlStatement,
+			&aItem.Url(), titleBuf, descBuf, &aItem.ImageUrl(), &aItem.ImageFileName(), &aItem.Link(),
+			aItem.BuildDate().Int64(), aItem.LastUpdated().Int64(), EAudioPodcast, aItem.CustomTitle(), aItem.LastError(), aItem.Uid());
+
+	CleanupStack::PopAndDestroy(descBuf);
+	CleanupStack::PopAndDestroy(titleBuf);
+	
+	sqlite3_stmt *st;
+	 
+	//DP1("SQL statement length=%d", iSqlBuffer.Length());
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st, (const void**) NULL);
+	
+	if (rc==SQLITE_OK)
+		{
+		rc = sqlite3_step(st);
+		sqlite3_finalize(st);
+		
+		if (rc == SQLITE_DONE)
+			{
+			return ETrue;
+			}
+		}
+	else
+		{
+		DP1("SQLite rc=%d", rc);
+		}
+
+	return EFalse;
+	}
+
+void CFeedEngine::ParsingCompleteL(CFeedInfo *item)
+	{
+	TBuf<KMaxLineLength> title;
+	title.Copy(item->Title());
+	TRAP_IGNORE(item->SetTitleL(title));
+	//DBUpdateFeed(*item);
+	}
+
+
+EXPORT_C void CFeedEngine::AddObserver(MFeedEngineObserver *observer)
+	{
+	iObservers.Append(observer);
+	}
+
+EXPORT_C void CFeedEngine::RemoveObserver(MFeedEngineObserver *observer)
+	{
+	TInt index = iObservers.Find(observer);
+	
+	if (index > KErrNotFound)
+		{
+		iObservers.Remove(index);
+		}
+	}
+
+void CFeedEngine::Connected(CHttpClient* /*aClient*/)
+	{
+	}
+
+void CFeedEngine::Progress(CHttpClient* /*aHttpClient*/, TInt /*aBytes*/, TInt /*aTotalBytes*/)
+	{	
+}
+
+void CFeedEngine::CompleteL(CHttpClient* /*aClient*/, TInt aError)
+	{
+	DP2("CFeedEngine::CompleteL BEGIN, iClientState=%d, aSuccessful=%d", iClientState, aError);
+
+	switch(iClientState)
+		{		
+		default:
+			if(iActiveFeed != NULL)
+				{
+				TTime time;
+				time.HomeTime();
+				iActiveFeed->SetLastUpdated(time);
+				iActiveFeed->SetLastError(aError);
+				NotifyFeedUpdateComplete(aError);
+				}
+			break;
+		case EUpdatingFeed: 
+		{
+		// Parse the feed. We need to trap this call since it could leave and then
+		// the whole update chain will be broken
+		// change client state
+		iClientState = EIdle;
+		switch (aError)
+			{
+			case KErrCancel:						
+				{
+				iFeedsUpdating.Reset();
+				}
+				break;
+			case KErrCouldNotConnect:
+				iFeedsUpdating.Reset();
+				break;
+			default:
+				{
+				iActiveFeed->SetLastError(aError);
+				TTime time;
+				time.HomeTime();
+				iActiveFeed->SetLastUpdated(time);
+
+				if( aError == KErrNone)
+					{			
+					TRAPD(parserErr, iParser->ParseFeedL(iUpdatingFeedFileName, iActiveFeed, iPodcastModel.SettingsEngine().MaxListItems()));
+
+					if(parserErr)
+						{
+						// we do not need to any special action on this error.
+						iActiveFeed->SetLastError(parserErr);
+						DP1("CFeedEngine::Complete()\t Failed to parse feed. Leave with error code=%d", parserErr);
+						}
+					else
+						{
+						iPodcastModel.ShowEngine().DeleteOldShowsByFeed(iActiveFeed->Uid());
+						}
+
+					// delete the downloaded XML file as it is no longer needed
+					BaflUtils::DeleteFile(iPodcastModel.FsSession(),iUpdatingFeedFileName);			
+
+					// if the feed has specified a image url. download it if we dont already have it
+					if((iActiveFeed->ImageUrl().Length() > 0))
+						{
+						if ( (iActiveFeed->ImageFileName().Length() == 0) || 
+								(iActiveFeed->ImageFileName().Length() > 0 && 
+										!BaflUtils::FileExists(iPodcastModel.FsSession(), 
+												iActiveFeed->ImageFileName()) )
+						)
+							{
+							TRAPD(error, GetFeedImageL(iActiveFeed));
+							if (error)
+								{
+								// we have failed in a very early stage to fetch the image.
+								// continue with next Feed update	
+								iActiveFeed->SetLastError(parserErr);
+								iClientState = EIdle;							
+								}
+							}	
+						}
+					}
+				}break;
+			}
+		
+		NotifyFeedUpdateComplete(aError);
+
+		// we will wait until the image has been downloaded to start the next feed update.
+		if (iClientState == EIdle)
+			{
+			UpdateNextFeedL();	
+			}
+		}break;
+	case EUpdatingImage:
+		{
+		// change client state to not updating
+		iClientState = EIdle;
+
+		NotifyFeedUpdateComplete(aError);
+		UpdateNextFeedL();
+		}break;
+	case ESearching: 
+		{
+		iClientState = EIdle;
+
+		DP2("Search complete, results in %S with error %d", &iSearchResultsFileName, aError);
+		if(aError == KErrNone)
+			{
+			if (!iOpmlParser) 
+				{
+				iOpmlParser = new COpmlParser(*this, iPodcastModel.FsSession());
+				}
+
+			DP("Parsing OPML");
+			iOpmlParser->ParseOpmlL(iSearchResultsFileName, ETrue);
+			}
+		
+		BaflUtils::DeleteFile(iPodcastModel.FsSession(), iSearchResultsFileName);
+		}break;
+		}
+	DP("CFeedEngine::CompleteL END");
+	}
+
+void CFeedEngine::NotifyFeedUpdateComplete(TInt aError)
+	{
+	DP("CFeedEngine::NotifyFeedUpdateComplete");
+	DBUpdateFeed(*iActiveFeed);
+	for (TInt i=0;i<iObservers.Count();i++) 
+		{
+		TRAP_IGNORE(iObservers[i]->FeedDownloadFinishedL(iAutoUpdatedInitiator?MFeedEngineObserver::EFeedAutoUpdate:MFeedEngineObserver::EFeedManualUpdate, iActiveFeed->Uid(), aError));
+		}
+	}
+
+void CFeedEngine::Disconnected(CHttpClient* /*aClient*/)
+	{
+	}
+
+void CFeedEngine::DownloadInfo(CHttpClient* /*aHttpClient */, int /*aTotalBytes*/)
+	{	
+	/*DP1("About to download %d bytes", aTotalBytes);
+	if(aHttpClient == iShowClient && iShowDownloading != NULL && aTotalBytes != -1) {
+		iShowDownloading->iShowSize = aTotalBytes;
+		}*/
+	}
+
+EXPORT_C void CFeedEngine::ImportFeedsL(const TDesC& aFile)
+	{
+	TFileName opmlPath;
+	opmlPath.Copy(aFile);
+	
+	if (!iOpmlParser) {
+		iOpmlParser = new COpmlParser(*this, iPodcastModel.FsSession());
+	}
+	
+	iOpmlParser->ParseOpmlL(opmlPath, EFalse);
+	}
+
+EXPORT_C TBool CFeedEngine::ExportFeedsL(TFileName& aFile)
+	{
+	RFile rfile;
+	TFileName privatePath;
+	iPodcastModel.FsSession().PrivatePath(privatePath);
+	TInt error = rfile.Temp(iPodcastModel.FsSession(), privatePath, aFile, EFileWrite);
+	if (error != KErrNone) 
+		{
+		DP("CFeedEngine::ExportFeedsL()\tFailed to open file");
+		return EFalse;
+		}
+	CleanupClosePushL(rfile);
+	
+	HBufC* templ = HBufC::NewLC(KMaxLineLength);
+	templ->Des().Copy(KOpmlFeed());
+	
+	HBufC* line = HBufC::NewLC(KMaxLineLength);
+	HBufC* xmlUrl = HBufC::NewLC(KMaxURLLength);		
+	HBufC* htmlUrl = HBufC::NewLC(KMaxURLLength);		
+	HBufC* desc = HBufC::NewLC(KMaxDescriptionLength);
+	HBufC* title = HBufC::NewLC(KMaxTitleLength);
+
+	HBufC8* utf8line = CnvUtfConverter::ConvertFromUnicodeToUtf8L(KOpmlHeader());
+
+	rfile.Write(*utf8line);
+	delete utf8line;
+	
+	for (int i=0; i<iSortedFeeds.Count(); i++)
+		{
+		CFeedInfo *info = iSortedFeeds[i];
+		DP1("Exporting feed '%S'", &iSortedFeeds[i]->Title());
+		
+	// XML URL
+		TPtr ptrXml(xmlUrl->Des());
+		if (info->Url() != KNullDesC)
+			{
+			ptrXml.Copy(info->Url());
+			PodcastUtils::XMLEncode(ptrXml);
+			}
+		
+	// Description	
+		TPtr ptrDesc(desc->Des());
+		ptrDesc.Zero();
+		if (info->Description() != KNullDesC) {
+			ptrDesc.Copy(info->Description());
+			PodcastUtils::XMLEncode(ptrDesc);
+		}
+		
+	// Title	
+		TPtr ptrTitle(title->Des());
+		ptrTitle.Zero();
+
+		if (info->Title() != KNullDesC) {
+			ptrTitle.Copy(info->Title());
+			PodcastUtils::XMLEncode(ptrTitle);
+		}
+		
+	// HTML URL	
+		TPtr ptrHtmlUrl(htmlUrl->Des());
+		ptrHtmlUrl.Zero();
+
+		if (info->Link() != KNullDesC) {
+			ptrHtmlUrl.Copy(info->Link());
+			PodcastUtils::XMLEncode(ptrHtmlUrl);
+		}	
+	// Write line to OPML file
+		line->Des().Format(*templ, title, desc, xmlUrl, htmlUrl);
+		utf8line = CnvUtfConverter::ConvertFromUnicodeToUtf8L(*line);
+		rfile.Write(*utf8line);
+		delete utf8line;
+		}
+
+	utf8line = CnvUtfConverter::ConvertFromUnicodeToUtf8L(KOpmlFooter());
+	rfile.Write(*utf8line);
+	delete utf8line;
+		
+	CleanupStack::PopAndDestroy(7);//destroy 6 bufs & close rfile
+	
+	return ETrue;
+	}
+
+EXPORT_C CFeedInfo* CFeedEngine::GetFeedInfoByUid(TUint aFeedUid)
+	{
+	TInt cnt = iSortedFeeds.Count();
+	for (TInt i=0;i<cnt;i++)
+		{
+		if (iSortedFeeds[i]->Uid() == aFeedUid)
+			{
+			return iSortedFeeds[i];
+			}
+		}
+	
+	return NULL;
+	}
+		
+EXPORT_C const RFeedInfoArray& CFeedEngine::GetSortedFeeds()
+{
+	TLinearOrder<CFeedInfo> sortOrder( CFeedEngine::CompareFeedsByTitle);
+
+	iSortedFeeds.Sort(sortOrder);
+	return iSortedFeeds;
+}
+
+TInt CFeedEngine::CompareFeedsByTitle(const CFeedInfo &a, const CFeedInfo &b)
+	{
+		//DP2("Comparing %S to %S", &a.Title(), &b.Title());
+		return a.Title().CompareF(b.Title());
+	}
+
+EXPORT_C void CFeedEngine::GetDownloadedStats(TUint &aNumShows, TUint &aNumUnplayed)
+	{
+	DP("CFeedEngine::GetDownloadedStats");
+	_LIT(KSqlStatement, "select count(*) from shows where downloadstate=%u");
+	iSqlBuffer.Format(KSqlStatement, EDownloaded);
+
+	sqlite3_stmt *st;
+	 
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+	
+	if( rc==SQLITE_OK ){
+	  	rc = sqlite3_step(st);
+	  	
+	  	if (rc == SQLITE_ROW) {
+	  		aNumShows = sqlite3_column_int(st, 0);
+	  	}
+	}
+		  
+	sqlite3_finalize(st);
+
+	_LIT(KSqlStatement2, "select count(*) from shows where downloadstate=%u and playstate=%u");
+	iSqlBuffer.Format(KSqlStatement2, EDownloaded, ENeverPlayed);
+
+	rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+		
+	if( rc==SQLITE_OK ){
+	  	rc = sqlite3_step(st);
+	  	
+	  	if (rc == SQLITE_ROW) {
+	  		aNumUnplayed = sqlite3_column_int(st, 0);
+	  	}
+	}
+		  
+	sqlite3_finalize(st);
+	}
+
+EXPORT_C void CFeedEngine::GetStatsByFeed(TUint aFeedUid, TUint &aNumShows, TUint &aNumUnplayed)
+	{
+	//DP1("CFeedEngine::GetStatsByFeed, aFeedUid=%u", aFeedUid);
+	DBGetStatsByFeed(aFeedUid, aNumShows, aNumUnplayed);
+	}
+
+void CFeedEngine::DBGetStatsByFeed(TUint aFeedUid, TUint &aNumShows, TUint &aNumUnplayed)
+	{
+	//DP1("CFeedEngine::DBGetStatsByFeed, feedUid=%u", aFeedUid);
+	_LIT(KSqlStatement, "select count(*) from shows where feeduid=%u");
+	iSqlBuffer.Format(KSqlStatement, aFeedUid);
+
+	sqlite3_stmt *st;
+	 
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+	
+	if( rc==SQLITE_OK ){
+	  	rc = sqlite3_step(st);
+	  	
+	  	if (rc == SQLITE_ROW) {
+	  		aNumShows = sqlite3_column_int(st, 0);
+	  	}
+	}
+		  
+	sqlite3_finalize(st);
+
+	_LIT(KSqlStatement2, "select count(*) from shows where feeduid=%u and playstate=0");
+	iSqlBuffer.Format(KSqlStatement2, aFeedUid);
+
+	rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+		
+	if( rc==SQLITE_OK ){
+	  	rc = sqlite3_step(st);
+	  	
+	  	if (rc == SQLITE_ROW) {
+	  		aNumUnplayed = sqlite3_column_int(st, 0);
+	  	}
+	}
+		  
+	sqlite3_finalize(st);
+}
+
+TUint CFeedEngine::DBGetFeedCount()
+	{
+	 DP("DBGetFeedCount BEGIN");
+	 sqlite3_stmt *st;
+	 int rc = sqlite3_prepare_v2(&iDB,"select count(*) from feeds" , -1, &st, (const char**) NULL);
+	 TUint size = 0;
+	 
+	 if( rc==SQLITE_OK ){
+	  	rc = sqlite3_step(st);
+	  	
+	  	if (rc == SQLITE_ROW) {
+	  		size = sqlite3_column_int(st, 0);
+	  	}
+	  }
+	  
+	  sqlite3_finalize(st);
+	  DP1("DBGetFeedCount END=%d", size);
+	  return size;
+}
+
+void CFeedEngine::DBLoadFeedsL()
+	{
+	DP("DBLoadFeeds BEGIN");
+	iSortedFeeds.Reset();
+	CFeedInfo *feedInfo = NULL;
+	
+	_LIT(KSqlStatement, "select url, title, description, imageurl, imagefile, link, built, lastupdated, uid, feedtype, customtitle from feeds");
+	iSqlBuffer.Format(KSqlStatement);
+
+	sqlite3_stmt *st;
+	 
+	TLinearOrder<CFeedInfo> sortOrder( CFeedEngine::CompareFeedsByTitle);
+
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+	Cleanup_sqlite3_finalize_PushL(st);
+	
+	if (rc==SQLITE_OK) {
+		rc = sqlite3_step(st);
+		while(rc == SQLITE_ROW) {
+			feedInfo = CFeedInfo::NewLC();
+			
+			const void *urlz = sqlite3_column_text16(st, 0);
+			TPtrC16 url((const TUint16*)urlz);
+			feedInfo->SetUrlL(url);
+
+			const void *titlez = sqlite3_column_text16(st, 1);
+			TPtrC16 title((const TUint16*)titlez);
+			feedInfo->SetTitleL(title);
+
+			const void *descz = sqlite3_column_text16(st, 2);
+			TPtrC16 desc((const TUint16*)descz);
+			feedInfo->SetDescriptionL(desc);
+
+			const void *imagez = sqlite3_column_text16(st, 3);
+			TPtrC16 image((const TUint16*)imagez);
+			feedInfo->SetImageUrlL(image);
+
+			const void *imagefilez = sqlite3_column_text16(st, 4);
+			TPtrC16 imagefile((const TUint16*)imagefilez);
+			feedInfo->SetImageFileNameL(imagefile);
+						
+			const void *linkz = sqlite3_column_text16(st, 5);
+			TPtrC16 link((const TUint16*)linkz);
+			feedInfo->SetDescriptionL(link);
+					
+			sqlite3_int64 built = sqlite3_column_int64(st, 6);
+			TTime buildtime(built);
+			feedInfo->SetBuildDate(buildtime);
+
+			sqlite3_int64 lastupdated = sqlite3_column_int64(st, 7);
+			TTime lastupdatedtime(lastupdated);
+			feedInfo->SetLastUpdated(lastupdatedtime);
+			
+			sqlite3_int64 customtitle = sqlite3_column_int64(st, 10);
+			if (customtitle) {
+				feedInfo->SetCustomTitle();
+			}
+			
+			TInt lasterror = sqlite3_column_int(st, 11);
+			feedInfo->SetLastError(lasterror);
+			
+			TLinearOrder<CFeedInfo> sortOrder( CFeedEngine::CompareFeedsByTitle);
+
+			iSortedFeeds.InsertInOrder(feedInfo, sortOrder);
+			
+			CleanupStack::Pop(feedInfo);
+				
+			rc = sqlite3_step(st);
+		}
+	}
+
+	CleanupStack::PopAndDestroy();//st
+
+	DP("DBLoadFeeds END");
+	}
+
+CFeedInfo* CFeedEngine::DBGetFeedInfoByUidL(TUint aFeedUid)
+	{
+	DP("CFeedEngine::DBGetFeedInfoByUid");
+	CFeedInfo *feedInfo = NULL;
+	_LIT(KSqlStatement, "select url, title, description, imageurl, imagefile, link, built, lastupdated, uid, feedtype, customtitle, lasterror from feeds where uid=%u");
+	iSqlBuffer.Format(KSqlStatement, aFeedUid);
+
+	sqlite3_stmt *st;
+	 
+	//DP1("SQL statement length=%d", iSqlBuffer.Length());
+
+	int rc = sqlite3_prepare16_v2(&iDB, (const void*)iSqlBuffer.PtrZ() , -1, &st,	(const void**) NULL);
+	
+	if (rc==SQLITE_OK) {
+		Cleanup_sqlite3_finalize_PushL(st);
+		rc = sqlite3_step(st);
+		if (rc == SQLITE_ROW) {
+			feedInfo = CFeedInfo::NewLC();
+			
+			const void *urlz = sqlite3_column_text16(st, 0);
+			TPtrC16 url((const TUint16*)urlz);
+			feedInfo->SetUrlL(url);
+
+			const void *titlez = sqlite3_column_text16(st, 1);
+			TPtrC16 title((const TUint16*)titlez);
+			feedInfo->SetTitleL(title);
+
+			const void *descz = sqlite3_column_text16(st, 2);
+			TPtrC16 desc((const TUint16*)descz);
+			feedInfo->SetDescriptionL(desc);
+
+			const void *imagez = sqlite3_column_text16(st, 3);
+			TPtrC16 image((const TUint16*)imagez);
+			feedInfo->SetImageUrlL(image);
+
+			const void *imagefilez = sqlite3_column_text16(st, 4);
+			TPtrC16 imagefile((const TUint16*)imagefilez);
+			feedInfo->SetDescriptionL(imagefile);
+			
+			const void *linkz = sqlite3_column_text16(st, 5);
+			TPtrC16 link((const TUint16*)linkz);
+			feedInfo->SetDescriptionL(link);
+					
+			sqlite3_int64 built = sqlite3_column_int64(st, 6);
+			TTime buildtime(built);
+			feedInfo->SetBuildDate(buildtime);
+
+			sqlite3_int64 lastupdated = sqlite3_column_int64(st, 7);
+			TTime lastupdatedtime(lastupdated);
+			feedInfo->SetLastUpdated(lastupdatedtime);
+			
+			sqlite3_int64 customtitle = sqlite3_column_int64(st, 10);
+			if (customtitle) {
+				feedInfo->SetCustomTitle();
+			}
+			
+			TInt lasterror = sqlite3_column_int(st, 11);
+			feedInfo->SetLastError(lasterror);
+						
+			CleanupStack::Pop(feedInfo);
+		}
+		CleanupStack::PopAndDestroy();//st	
+	}
+	
+	return feedInfo;
+}
+
+EXPORT_C void CFeedEngine::UpdateFeed(CFeedInfo *aItem)
+	{
+	DBUpdateFeed(*aItem);
+	}
+
+EXPORT_C void CFeedEngine::SearchForFeedL(TDesC& aSearchString)
+	{
+	DP1("FeedEngine::SearchForFeedL BEGIN, aSearchString=%S", &aSearchString);
+	
+	if (iClientState != EIdle) {
+		User::Leave(KErrInUse);
+	}
+	TBuf<KMaxURLLength> ssBuf;
+	ssBuf.Copy(aSearchString);
+	PodcastUtils::ReplaceString(ssBuf, _L(" "), _L("%20"));
+	// prepare search URL
+	HBufC* templ = HBufC::NewLC(KMaxLineLength);
+	templ->Des().Copy(KSearchUrl());
+		
+	HBufC* url = HBufC::NewLC(KMaxURLLength);		
+	url->Des().Format(*templ, &ssBuf);
+
+	DP1("SearchURL: %S", url);
+	
+	// crate path to store OPML search results
+	iPodcastModel.FsSession().PrivatePath(iSearchResultsFileName);
+	
+	iSearchResultsFileName.Append(KSearchResultsFileName);
+	iSearchResults.ResetAndDestroy();
+	// run search
+	if(iFeedClient->GetL(*url, iSearchResultsFileName, iPodcastModel.SettingsEngine().SpecificIAP()))
+		{
+		iClientState = ESearching;
+		}
+	else
+		{
+		iClientState = EIdle;
+		User::Leave(KErrAbort);
+		}
+	
+	CleanupStack::PopAndDestroy(url);
+	CleanupStack::PopAndDestroy(templ);
+		
+	DP("FeedEngine::SearchForFeedL END");
+	}
+
+EXPORT_C void CFeedEngine::AddSearchResultL(CFeedInfo *item)
+	{
+	DP1("CFeedEngine::AddSearchResultL, item->Title()=%S", &(item->Title()));
+	iSearchResults.AppendL(item);
+	}
+
+EXPORT_C const RFeedInfoArray& CFeedEngine::GetSearchResults()
+	{
+	return iSearchResults;
+	}
+
+
+EXPORT_C void CFeedEngine::OpmlParsingComplete(TUint aNumFeedsAdded)
+	{
+	NotifyOpmlParsingComplete(aNumFeedsAdded);
+	}
+
+void CFeedEngine::NotifyOpmlParsingComplete(TUint aNumFeedsAdded)
+	{
+	for (TInt i=0;i<iObservers.Count();i++) 
+		{
+		iObservers[i]->OpmlParsingComplete(aNumFeedsAdded);
+		}
+	}