package org.terst.feedreader.opml import android.util.Xml import org.terst.feedreader.data.FeedEntity import org.xmlpull.v1.XmlPullParser import java.io.InputStream import java.io.OutputStream data class OpmlFeed(val title: String, val xmlUrl: String, val htmlUrl: String?, val folder: String? = null) /** * OPML import/export. Folder nesting ( containing other * elements instead of an xmlUrl) is captured as a single folder name per * feed -- the nearest enclosing container outline's title/text. Nested * folders-within-folders collapse to just the innermost name; this app * groups by one folder level, not a full category tree. */ object Opml { fun parse(input: InputStream): List { val parser = Xml.newPullParser() parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) parser.setInput(input, null) val feeds = mutableListOf() // One entry pushed per START_TAG, popped on its END_TAG -- a folder // container's entry holds its name, a feed (or any other) outline's is null. // The current folder is whichever named entry is nearest the top of the stack. val outlineStack = mutableListOf() var eventType = parser.eventType while (eventType != XmlPullParser.END_DOCUMENT) { when (eventType) { XmlPullParser.START_TAG -> if (parser.name.equals("outline", ignoreCase = true)) { val xmlUrl = parser.getAttributeValue(null, "xmlUrl") if (xmlUrl != null) { val title = parser.getAttributeValue(null, "title") ?: parser.getAttributeValue(null, "text") ?: xmlUrl val htmlUrl = parser.getAttributeValue(null, "htmlUrl") val folder = outlineStack.filterNotNull().lastOrNull() feeds += OpmlFeed(title, xmlUrl, htmlUrl, folder) outlineStack += null } else { val folderName = parser.getAttributeValue(null, "text") ?: parser.getAttributeValue(null, "title") outlineStack += folderName } } XmlPullParser.END_TAG -> if (parser.name.equals("outline", ignoreCase = true) && outlineStack.isNotEmpty()) { outlineStack.removeAt(outlineStack.size - 1) } } eventType = parser.next() } return feeds } fun write(feeds: List, output: OutputStream) { val serializer = Xml.newSerializer() serializer.setOutput(output, "UTF-8") serializer.startDocument("UTF-8", true) serializer.startTag(null, "opml") serializer.attribute(null, "version", "2.0") serializer.startTag(null, "head") serializer.startTag(null, "title") serializer.text("Feeds subscriptions") serializer.endTag(null, "title") serializer.endTag(null, "head") serializer.startTag(null, "body") fun writeFeedOutline(feed: FeedEntity) { serializer.startTag(null, "outline") serializer.attribute(null, "text", feed.title) serializer.attribute(null, "title", feed.title) serializer.attribute(null, "type", "rss") serializer.attribute(null, "xmlUrl", feed.id) feed.siteUrl?.let { serializer.attribute(null, "htmlUrl", it) } serializer.endTag(null, "outline") } val (grouped, ungrouped) = feeds.partition { it.folder != null } for ((folder, feedsInFolder) in grouped.groupBy { it.folder }) { serializer.startTag(null, "outline") serializer.attribute(null, "text", folder) serializer.attribute(null, "title", folder) for (feed in feedsInFolder) writeFeedOutline(feed) serializer.endTag(null, "outline") } for (feed in ungrouped) writeFeedOutline(feed) serializer.endTag(null, "body") serializer.endTag(null, "opml") serializer.endDocument() serializer.flush() } }