Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-android-extensions")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.okhttp)
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="de.mm20.rssparser"/>
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.mm20.rssparser
|
||||
|
||||
/**
|
||||
* Represents an <item> of the RSS feed.
|
||||
* A channel may contain any number of <item>s. An item may represent a "story" -- much like a story
|
||||
* in a newspaper or magazine; if so its description is a synopsis of the story, and the link points
|
||||
* to the full story. An item may also be complete in itself, if so, the description contains the
|
||||
* text (entity-encoded HTML is allowed), and the link and title may be omitted. All elements of an
|
||||
* item are optional, however at least one of title or description must be present.
|
||||
*/
|
||||
data class Article(
|
||||
/**
|
||||
* The title of the item.
|
||||
*/
|
||||
val title: String,
|
||||
/**
|
||||
* The URL of the item.
|
||||
*/
|
||||
val link: String,
|
||||
/**
|
||||
* The item synopsis.
|
||||
*/
|
||||
val description: String,
|
||||
/**
|
||||
* Email address of the author of the item.
|
||||
*/
|
||||
val author: String?,
|
||||
/**
|
||||
* Includes the item in one or more categories.
|
||||
*/
|
||||
val categories: List<String>,
|
||||
/**
|
||||
* URL of a page for comments relating to the item.
|
||||
*/
|
||||
val comments: String?,
|
||||
/**
|
||||
* Describes a media object that is attached to the item.
|
||||
*/
|
||||
val enclosure: String?,
|
||||
/**
|
||||
* A string that uniquely identifies the item.
|
||||
*/
|
||||
val guid: String?,
|
||||
/**
|
||||
* Indicates when the item was published.
|
||||
*/
|
||||
val pubDate: Long?,
|
||||
/**
|
||||
* The RSS channel that the item came from.
|
||||
*/
|
||||
val source: String?
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.rssparser
|
||||
|
||||
enum class DayOfWeek {
|
||||
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.mm20.rssparser
|
||||
|
||||
class InvalidFeedException(message: String? = null) : Exception(message)
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.mm20.rssparser
|
||||
|
||||
data class FeedInfo(
|
||||
/**
|
||||
* The name of the channel. It's how people refer to your service. If you have an HTML
|
||||
* website that contains the same information as your RSS file, the title of your channel
|
||||
* should be the same as the title of your website.
|
||||
*/
|
||||
val title: String,
|
||||
/**
|
||||
* The URL to the HTML website corresponding to the channel.
|
||||
*/
|
||||
val link: String,
|
||||
/**
|
||||
* Phrase or sentence describing the channel.
|
||||
*/
|
||||
val description: String,
|
||||
/**
|
||||
* The language the channel is written in. This allows aggregators to group all Italian
|
||||
* language sites, for example, on a single page. A list of allowable values for this
|
||||
* element, as provided by Netscape, is
|
||||
* [here](http://backend.userland.com/stories/storyReader$16). You may also use
|
||||
* [values defined](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) by the
|
||||
* W3C.
|
||||
*/
|
||||
val language: String?,
|
||||
/**
|
||||
* Copyright notice for content in the channel.
|
||||
*/
|
||||
val copyright: String?,
|
||||
/**
|
||||
* Email address for person responsible for editorial content.
|
||||
*/
|
||||
val managingEditor: String?,
|
||||
/**
|
||||
* Email address for person responsible for technical issues relating to channel.
|
||||
*/
|
||||
val webMaster: String?,
|
||||
/**
|
||||
* The publication date for the content in the channel. For example, the New York Times
|
||||
* publishes on a daily basis, the publication date flips once every 24 hours. That's when
|
||||
* the pubDate of the channel changes.
|
||||
*/
|
||||
val pubDate: Long?,
|
||||
/**
|
||||
* The last time the content of the channel changed.
|
||||
*/
|
||||
val lastBuildDate: Long?,
|
||||
/**
|
||||
* Specify one or more categories that the channel belongs to.
|
||||
*/
|
||||
val category: List<String>,
|
||||
/**
|
||||
* A string indicating the program used to generate the channel.
|
||||
*/
|
||||
val generator: String?,
|
||||
/**
|
||||
* A URL that points to the documentation for the format used in the RSS file. It's probably
|
||||
* a pointer to [this page](https://validator.w3.org/feed/docs/rss2.html). It's for people
|
||||
* who might stumble across an RSS file on a Web server 25 years from now and wonder what it
|
||||
* is.
|
||||
*/
|
||||
val docs: String?,
|
||||
/**
|
||||
* ttl stands for time to live. It's a number of minutes that indicates how long a channel
|
||||
* can be cached before refreshing from the source. More info
|
||||
* [here](https://validator.w3.org/feed/docs/rss2.html#ltttlgtSubelementOfLtchannelgt).
|
||||
*/
|
||||
val ttl: Int?,
|
||||
/**
|
||||
* Specifies a GIF, JPEG or PNG image that can be displayed with the channel.
|
||||
*/
|
||||
val image: String?,
|
||||
/**
|
||||
* A hint for aggregators telling them which hours they can skip. More info
|
||||
* [here](http://backend.userland.com/skipHoursDays#skiphours).
|
||||
*/
|
||||
val skipHours: List<Int>,
|
||||
/**
|
||||
* A hint for aggregators telling them which days they can skip. More info
|
||||
* [here](http://backend.userland.com/skipHoursDays#skipdays).
|
||||
*/
|
||||
val skipDays: List<DayOfWeek>
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
package de.mm20.rssparser
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.w3c.dom.Element
|
||||
import org.xml.sax.SAXException
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class RssParser(
|
||||
var url: String
|
||||
) {
|
||||
|
||||
lateinit var articles: List<Article>
|
||||
private set
|
||||
|
||||
lateinit var feedInfo: FeedInfo
|
||||
private set
|
||||
|
||||
/**
|
||||
* Fetches the feed from server and parses it.
|
||||
* Do not call this in main thread.
|
||||
* After this you can access [feedInfo] and [articles]
|
||||
* @return the list of articles in that RSS feed
|
||||
* @throws InvalidFeedException if the url does not point to a valid RSS2.0 feed
|
||||
* @throws java.io.IOException if the feed url does not exist or there was an error downloading it
|
||||
*/
|
||||
@Throws(InvalidFeedException::class, IOException::class)
|
||||
fun loadFeed() {
|
||||
val articles = mutableListOf<Article>()
|
||||
try {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val client = OkHttpClient()
|
||||
val feedStream = client.newCall(request).execute().body?.byteStream()
|
||||
?: throw IOException()
|
||||
val document = DocumentBuilderFactory
|
||||
.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(feedStream)
|
||||
|
||||
val rootElement = document.documentElement
|
||||
|
||||
if (rootElement.tagName != "rss" || rootElement.getAttribute("version") != "2.0") {
|
||||
throw InvalidFeedException("Document is not an RSS 2.0 feed")
|
||||
}
|
||||
val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ROOT)
|
||||
feedInfo = FeedInfo(
|
||||
title = document.getElementsByTagName("title").item(0)?.textContent ?: url,
|
||||
link = document.getElementsByTagName("link").item(0)?.textContent ?: url,
|
||||
description = document.getElementsByTagName("description").item(0)?.textContent
|
||||
?: "",
|
||||
language = document.getElementsByTagName("language").item(0)?.textContent,
|
||||
copyright = document.getElementsByTagName("copyright").item(0)?.textContent,
|
||||
managingEditor = document.getElementsByTagName("managingEditor").item(0)?.textContent,
|
||||
webMaster = document.getElementsByTagName("webMaster").item(0)?.textContent,
|
||||
pubDate = parseDate(document.getElementsByTagName("pubDate")
|
||||
.item(0)?.textContent ?: "", dateFormat),
|
||||
lastBuildDate = parseDate(document.getElementsByTagName("lastBuildDate")
|
||||
.item(0)?.textContent ?: "", dateFormat),
|
||||
category = document.getElementsByTagName("category").run {
|
||||
(0 until length).mapNotNull { item(it).textContent }
|
||||
},
|
||||
generator = document.getElementsByTagName("generator").item(0)?.textContent,
|
||||
docs = document.getElementsByTagName("docs").item(0)?.textContent,
|
||||
ttl = document.getElementsByTagName("generator").item(0)?.textContent?.toIntOrNull(),
|
||||
image = (document.getElementsByTagName("image")
|
||||
.item(0)as? Element)
|
||||
?.getElementsByTagName("url")
|
||||
?.item(0)?.textContent,
|
||||
skipHours = (document.getElementsByTagName("skipHours") as? Element)
|
||||
?.getElementsByTagName("hour")?.run {
|
||||
(0 until length).mapNotNull { item(it)?.textContent?.toIntOrNull() }
|
||||
} ?: emptyList(),
|
||||
skipDays = (document.getElementsByTagName("skipDays") as? Element)
|
||||
?.getElementsByTagName("day")?.run {
|
||||
(0 until length).mapNotNull { getWeekday(item(it)?.textContent) }
|
||||
} ?: emptyList()
|
||||
)
|
||||
val items = document.getElementsByTagName("item")
|
||||
|
||||
for (i in 0 until items.length) {
|
||||
val item = items.item(i) as? Element ?: continue
|
||||
val article = Article(
|
||||
title = item.getElementsByTagName("title").item(0)?.textContent ?: continue,
|
||||
link = item.getElementsByTagName("link").item(0)?.textContent ?: "",
|
||||
description = item.getElementsByTagName("description").item(0)?.textContent
|
||||
?: "",
|
||||
author = item.getElementsByTagName("author").item(0)?.textContent,
|
||||
categories = document.getElementsByTagName("category").run {
|
||||
(0 until length).mapNotNull { item(it).textContent }
|
||||
},
|
||||
comments = item.getElementsByTagName("comments").item(0)?.textContent,
|
||||
enclosure = item.getElementsByTagName("enclosure").item(0)?.textContent,
|
||||
guid = item.getElementsByTagName("guid").item(0)?.textContent,
|
||||
pubDate = parseDate(item.getElementsByTagName("pubDate").item(0)?.textContent
|
||||
?: "", dateFormat),
|
||||
source = item.getElementsByTagName("source").item(0)?.textContent
|
||||
)
|
||||
articles.add(article)
|
||||
}
|
||||
|
||||
} catch (e: SAXException) {
|
||||
throw InvalidFeedException()
|
||||
} catch (e: IOException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
this.articles = articles
|
||||
}
|
||||
|
||||
private fun parseDate(date: String, dateFormat: SimpleDateFormat): Long? {
|
||||
return try {
|
||||
dateFormat.parse(date)?.time
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWeekday(day: String?): DayOfWeek? {
|
||||
return when (day) {
|
||||
"Sunday" -> DayOfWeek.SUNDAY
|
||||
"Monday" -> DayOfWeek.MONDAY
|
||||
"Tuesday" -> DayOfWeek.TUESDAY
|
||||
"Wednesday" -> DayOfWeek.WEDNESDAY
|
||||
"Thursday" -> DayOfWeek.SATURDAY
|
||||
"Friday" -> DayOfWeek.FRIDAY
|
||||
"Saturday" -> DayOfWeek.SATURDAY
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">RSSParser</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user