An RSS reader for FreshRSS with ranking, sorting, and design to provide a Google Discover-like experience from a personally curated RSS repository.
  • Kotlin 99.1%
  • Shell 0.9%
Find a file
2026-07-27 16:29:15 -04:00
app Bump version to v0.0.58 2026-07-27 16:29:15 -04:00
gradle newPipeExtractor update to v0.26.4 to fix streaming 2026-07-21 01:10:30 -04:00
.env build release 2026-07-16 22:14:46 -04:00
.gitignore Fix article rendering: WebView reader body (images, headings, YouTube embeds), strip obj/control artifacts, robust card hero images with YouTube hqdefault fallback 2026-07-16 21:31:37 -04:00
build.gradle.kts Initial release v0.0.1 2026-07-12 05:01:02 -04:00
crash_log.txt [ranking] Release v0.0.56 with zero-allocation duplicate clustering fix 2026-07-25 22:47:14 -04:00
follow-up-features.md Fix add feed feature not extracting content 2026-07-19 14:49:46 -04:00
gradle.properties Initial release v0.0.1 2026-07-12 05:01:02 -04:00
gradlew Initial release v0.0.1 2026-07-12 05:01:02 -04:00
gradlew.bat Initial release v0.0.1 2026-07-12 05:01:02 -04:00
logcat.txt Another fix fix. 2026-07-17 23:31:03 -04:00
project-prep.sh Initial release v0.0.1 2026-07-12 05:01:02 -04:00
README.md "Redesign icon. Add left-side drawer for category and subscription navigation/view" 2026-07-15 15:52:50 -04:00
settings.gradle.kts Replace Invidious with NewPipeExtractor built in. Remove YouTube embed in favor of native video player. 2026-07-12 17:06:56 -04:00
VERSION Bump version to v0.0.58 2026-07-27 16:29:15 -04:00

FresshReader

Your feeds. Your rules.

A privacy-first Android RSS reader that pairs with your FreshRSS server and delivers a Google Discoverstyle browsing experience — entirely on-device. Your data never leaves your device.

Every article, feed, sync, and ranking decision in FresshReader is fully auditable. The source code is open, the algorithms are documented, and nothing happens in secret.


Table of Contents


Features

  • FreshRSS Greader API sync — authenticate, pull subscriptions and articles, push read/star state back
  • Discover feed — algorithmic ranking with recency scoring, feed weighting, source cooldown, category balancing, and duplicate clustering for a varied, curated experience
  • Five feed modes — Discover (Balanced / Priority), Latest (one per source), Starred, Catch Up (oldest first)
  • On-device ranking — fully deterministic, no cloud round-trips, every placement has an explainable reason
  • Offline support — articles are cached locally and fully readable without network
  • Background sync — periodic sync every 30 minutes via Android WorkManager
  • Pull-to-refresh — manual sync whenever you want new content
  • Inline video player — YouTube videos detected in your feed are played inside the reader via Invidious (ad-free), with optional SponsorBlock auto-skipping of sponsored segments
  • Explainability — every article card shows why it was placed there (recency, favorite source, category diversity, etc.)
  • Star feed sources — mark entire feeds as "starred" and see their articles alongside your bookmarked articles in the Starred tab
  • Feed management — set source weight (Favorite / Normal / Low / Muted), cooldown spacing, and preferred-source status from Settings
  • Left side drawer — swipe from the left edge (or tap the menu icon) to open a slide-out drawer. The top section lists every category; the bottom section lists your subscriptions, grouped by category with per-category collapsible groups
  • Category & subscription views — pick a category or a single subscription from the drawer to open a full-page, Discover-style browse of just those articles. Both support search within the selection, sorting (newest first / oldest first / random), and — for categories — a source filter (multi-select chips of the sources in that category)
  • System theme support — light, dark, or follow-the-system
  • Custom accent color — pick from a palette of colors to personalize the app

How It Works

FresshReader does not fetch, parse, or store RSS feeds on its own. It relies entirely on your FreshRSS server for feed retrieval and storage. The app is a smart browsing layer on top of FreshRSS.

Data Flow

  FreshRSS Server
  (fetches feeds, parses RSS/Atom)
       │
       ▼  Greader API (HTTPS)
  ┌─────────────┐
  │  FresshReader │
  │  (Android app)│
  │               │
  │  Local Room DB│
  │  (on-device   │
  │   cache)      │
  │               │
  │  Ranking      │
  │  Engine       │
  │  (on-device)  │
  │               │
  │  Compose UI   │
  └─────────────┘
  1. Login — You provide your FreshRSS server URL, username, and password (or API password). The app authenticates via FreshRSS's Google Readerstyle ClientLogin endpoint and stores your credentials in Android's encrypted storage (EncryptedSharedPreferences).

  2. Initial sync — After authentication, the app pulls your full subscription list via subscription/list. For each subscription, it fetches the latest 100 articles (with pagination) via stream/contents. All data is written to a local Room SQLite database.

  3. Incremental sync — Subsequent syncs (manual or background) use a timestamp-based approach. The app stores the last sync time and only pulls articles published after that point. FreshRSS handles deduplication.

  4. Ranking — When you open a feed, the app queries the local database for candidate articles, then runs the ranking pipeline entirely on-device (the CPU — no network calls). The ranked list is displayed as cards.

  5. State push-back — When you mark an article read or toggle its star, the app pushes that state back to FreshRSS via the edit-tag endpoint, so changes stay in sync across all your RSS clients.

  6. Offline fallback — Articles already cached in Room are available for reading at any time, with or without network. Sync operations gracefully handle network errors and will retry on the next interval.

The Local Database (Room)

All synced data lives in a local SQLite database managed by Room (Android's official persistence library). Three tables are maintained:

Table What it stores Why it exists
feeds Subscription metadata: ID, name, URL, icon, category, weight, cooldown, preferred/hidden/starred flags In-mirrors your FreshRSS subscriptions; the app extends this with local-only fields (weight, cooldown, etc.) that never sync back
articles Full article data: title, summary, content, author, published date, URL, categories, enclosure links, images, read/star state, ranking score, display position, duplicate-group ID Enables offline reading and powers the ranking engine without hitting the network
sync_state Key-value store for sync metadata (last successful sync timestamp, auth token) Tracks where you left off for incremental sync

Feed Processing

When data arrives from FreshRSS:

  • FreshRSS is the authority for what feeds exist and what articles belong to them
  • FresshReader preserves all local-only fields (weight, cooldown, hidden/starred state, ranking score, display position) across syncs — they are never overwritten
  • If an article already exists locally, the app smart-merges it (preserving read/star state, local score, and other app-specific data)
  • Duplicate detection runs at ranking time, not sync time, so deduplication adapts as articles age

Communication & Security

Concern How it works
Protocol FreshRSS Google Reader API (Greader) over HTTPS
Authentication ClientLogin — username + password exchanged once for an Auth token; the token is sent as a bearer header on all subsequent requests
Credential storage EncryptedSharedPreferences (AES-256-GCM backed by Android Keystore) — your password and auth token are encrypted at rest
Credential transmission Password is sent once during login, over HTTPS, to your FreshRSS server only
In-memory auth The auth token is held in memory for the app's lifetime; the raw password is loaded from encrypted storage only when a new token is needed
Network requests All API calls go to your FreshRSS server — nothing is sent to any third party
Encryption used AES-256-GCM (credential storage), TLS 1.3 (network transport via OkHttp)
FreshRSS endpoints used accounts/ClientLogin, reader/api/0/subscription/list, reader/api/0/stream/contents, reader/api/0/edit-tag
Fever API fallback If your FreshRSS version does not support Greader subscription/list, the app falls back to the Fever API to enumerate feeds and relies on Greader for article content

What the app sends to your server

  • Your username and password (once, at login)
  • An Authorization: GoogleLogin auth=... header on all requests
  • Read/star state changes as edit-tag calls when you interact with articles

What the app sends to third parties

  • SponsorBlock API (sponsor.ajay.app) — when you watch a YouTube video inside the app, a request is made to fetch skip segments (sponsored content timestamps). No identifying data is sent; only the YouTube video ID. You can read SponsorBlock's privacy policy at https://sponsor.ajay.app/privacy.
  • YouTube/Invidious — when watching a YouTube video, the WebView loads an embed page from a randomly chosen Invidious instance (see YouTube & Video Playback). Invidious serves no ads, runs no tracking scripts, and does not require JavaScript from Google's domains.
  • Coil image loader — article images (thumbnails, feed icons) are fetched from the URLs provided by your RSS feeds. This typically means connecting to the original publisher's servers to download images. Coil does not send identifying headers beyond standard HTTP.

Third-Party Dependencies

Every external library in FresshReader is included for a specific purpose, and every one is open-source. Here is the complete list, what it does, and why it is here.

Build & Compilation

Dependency Version What it does Why it is included
Kotlin 1.9.24 The programming language Primary development language for Android
Android Gradle Plugin 8.5.2 Build system plugin Compiles the Android app, manages the SDK toolchain

UI Framework (Jetpack Compose + Material 3)

Dependency Version What it does Why it is included
Compose BOM 2024.06.00 Bill of materials — pins all Compose libraries to compatible versions Ensures all Jetpack Compose libraries work together
Compose UI (via BOM) Core UI toolkit — layout, drawing, input, accessibility Declarative UI rendering engine
Compose Material 3 (via BOM) Material Design 3 components — cards, buttons, navigation bar, text fields, dialogs Provides the app's look and feel (Material 3 design language)
Material Icons Extended (via BOM) 5,000+ Material Design icon resources Powers all icons in the app (star, share, settings, play, etc.)
Compose UI Tooling (via BOM) Layout inspector and preview renderer Used during development for debugging UI layouts
Activity Compose 1.9.0 Bridges Android Activity lifecycle with Compose Required entry point for any Compose-based Android app
Navigation Compose 2.7.7 Declarative navigation — tab bar, screen routing, back stack Manages navigation between Discover, Latest, Starred, Catch Up, Settings, and Reader screens
Hilt Navigation Compose 1.2.0 Dependency injection for Compose Navigation destinations Injects ViewModels into each screen automatically

Architecture & Dependency Injection

Dependency Version What it does Why it is included
Hilt (Dagger) 2.51.1 Compile-time dependency injection framework Wires together the database, API client, sync engine, ranking engine, and ViewModels without manual boilerplate. Hilt is Google's recommended DI for Android.
androidx.lifecycle 2.8.2 Lifecycle-aware components — ViewModel, LiveData, LifecycleOwner Manages UI state across configuration changes, survives rotation, handles coroutine scoping
androidx.core.ktx 1.13.1 Kotlin extensions for Android core APIs Provides idiomatic Kotlin wrappers (e.g. Context.toast(), SharedPreferences.edit {})

Local Database

Dependency Version What it does Why it is included
Room 2.6.1 SQLite object-relational mapping (ORM) Abstracts the local database with compile-time verified queries. Room is Google's recommended persistence library for Android.
Room KTX 2.6.1 Kotlin coroutine extensions for Room Enables suspending DAO methods (suspend fun) for clean coroutine integration
Room Compiler 2.6.1 Annotation processor Generates Room implementation code at compile time

Network

Dependency Version What it does Why it is included
Retrofit 2.11.0 Type-safe HTTP client Declares the FreshRSS Greader API as a Kotlin interface; Retrofit handles serialization, deserialization, and HTTP method routing. It is the most widely used HTTP client in Android.
Gson Converter 2.11.0 JSON serialization/deserialization for Retrofit Converts FreshRSS JSON responses to Kotlin data classes (and vice versa for outgoing requests)
OkHttp 4.12.0 Low-level HTTP engine Powers Retrofit's actual network I/O; handles connection pooling, timeouts, TLS, and HTTP/2. OkHttp is the foundational networking layer used by most Android apps.
OkHttp Logging 4.12.0 HTTP request/response logging interceptor Used during debugging to inspect API calls; excluded from release builds via configuration

Background Work

Dependency Version What it does Why it is included
WorkManager 2.9.1 Deferrable background task scheduler Runs periodic sync every 30 minutes, surviving app restarts and device reboots. WorkManager is Google's recommended library for persistent background work in Android.

Image Loading

Dependency Version What it does Why it is included
Coil 2.6.0 Image loader for Compose Fetches, caches, and renders article images and feed icons. Coil is the most popular Compose-native image loader, built on OkHttp (which we already include). It handles disk/memory caching, downsampling, and placeholder rendering automatically.

Encryption

Dependency Version What it does Why it is included
AndroidX Security Crypto 1.0.0 EncryptedSharedPreferences Encrypts your server URL, username, password, and auth token at rest using AES-256-GCM backed by the Android Keystore hardware (where available)

Coroutines

Dependency Version What it does Why it is included
Kotlinx Coroutines 1.8.1 Kotlin coroutines — structured concurrency Powers all async operations: network calls, database queries, sync pipeline, ranking engine. Every suspending function in the app runs on a coroutine.
Kotlinx Coroutines Android 1.8.1 Android Dispatchers.Main integration Enables viewModelScope.launch(Dispatchers.IO) — the standard pattern for safe background work in ViewModels

Integration with SponsorBlock

Dependency Version What it does Why it is included
SponsorBlock API (external service) Crowd-sourced database of sponsored segments in YouTube videos Not a library. The app makes an HTTP GET to https://sponsor.ajay.app/api/skipSegments?videoID=... to fetch sponsored-segment timestamps. No identifying data is sent — only the YouTube video ID. Skip segments are optional and enabled per-video.

Testing (development only, not in the APK)

Dependency Version What it does
JUnit 4 4.13.2 Unit testing framework
MockK 1.13.11 Kotlin mocking library for tests
AndroidX Test JUnit 1.2.1 Android instrumentation test helpers
Espresso 3.6.1 UI automation testing

Installation

FresshReader is distributed as a signed APK attached to each release. The recommended way to install and receive updates is Obtainium, an APK downloader and updater that supports Forgejo releases natively.

  1. Install Obtainium on your Android device
  2. Open Obtainium and tap Add App
  3. Configure the source:
    • Source Type: Forgejo
    • App URL: https://forge.sabylasolutions.com/sabyla/fresshreader
  4. Tap Add — Obtainium fetches the latest release info
  5. Verify the package ID is com.fresshreader.app and tap Download/Install
  6. Automatic updates will be delivered through Obtainium thereafter

Manual install

Download the app-release-v*.apk from the releases page and install it directly.


Getting Started

  1. You need a FreshRSS instance. FresshReader connects to an existing FreshRSS server (self-hosted or shared). You need your server URL, username, and either your account password or a dedicated API password (recommended — set in FreshRSS under Profile → API password).

  2. Launch the app. The first screen is the login form:

    • Server URL — the full URL of your FreshRSS instance, e.g. https://rss.example.com (no trailing path needed; the app auto-appends /api/greader.php/)
    • Username — your FreshRSS login
    • Password — your account password or the dedicated API password
    • Tap Connect to authenticate
  3. First sync. After login, the app triggers an initial full sync: it pulls all your subscriptions and the latest articles. A loading indicator shows progress.

  4. Ongoing sync. The app automatically syncs every 30 minutes in the background. You can also pull-to-refresh on any feed to trigger an immediate sync.


Feed Modes

FresshReader offers five distinct ways to browse your feeds, accessible from the bottom navigation bar:

Mode Tab What it shows
Discover — Balanced 1st tab All non-muted articles, algorithmically ranked for variety. The default feed. Recency, source preference, category diversity, and source spacing are all factored in.
Discover — Priority 1st tab (sub-tab) Same pipeline as Balanced, but favorite sources are weighted more heavily. For when you want to prioritize your preferred feeds.
Latest 2nd tab One most-recent unread article per source, newest first. A simple chronological view.
Starred 3rd tab Articles you have bookmarked (starred), plus articles from feeds you have marked as starred. Useful as a "favorites" collection.
Catch Up 4th tab All unread articles, oldest first. Designed for working through your backlog.
Settings 5th tab (cog icon) Feed management (star, weight, cooldown, preferred source), theme (light/dark/system), accent color picker, account logout

Within each mode, tapping an article card opens the full Reader Screen with:

  • Formatted article content (HTML rendered via Android WebView)
  • Article metadata (author, date, source, estimated reading time)
  • YouTube / Invidious video player inline (with SponsorBlock support — see below)
  • Share button to share the article URL with any Android app
  • Swipe back or use the back arrow to return to the feed

The Left Drawer & Category / Subscription Views

Beyond the bottom navigation tabs, FresshReader has a left side drawer for drilling into your feeds by category and by individual source.

  • Open it by swiping from the left screen edge, or by tapping the menu (hamburger) icon in any tab's top bar.
  • Top section — Categories: every FreshRSS category (feeds with no category appear as "Uncategorized"). Tap one to open a full-page view of all articles in that category.
  • Bottom section — Subscriptions: your feeds, grouped by category. Each category group is collapsible; tap a feed to open a full-page view of all articles from that single source.

Both full-page views behave like the bottom-nav tabs (they keep the bottom navigation bar and open articles in the Reader). Within them you can:

  • Search — a search field filters articles by title/summary within the selected category or source.
  • Sort — newest first, oldest first, or random. Random order is fixed the moment you pick it and stays stable across refreshes (pull-to-refresh keeps its standard behavior).
  • Filter by source (categories only) — toggle chips to narrow the category view to one or more specific sources; no chips selected means "all sources in this category".

Unlike the main tabs (which show unread, deduped articles and exclude muted/hidden feeds), the category and subscription views show every article — read and unread, including muted and hidden feeds — so they act as a complete, unfiltered browser for that slice of your library.


The Ranking Engine

The Discover feeds use a multi-stage ranking pipeline that runs entirely on your device. Every placement is computed deterministically from the same data and is fully explainable.

Pipeline Stages

  1. Recentness scoring — Each article is assigned a recency score based on its age. An article published 5 minutes ago scores near 100; one published 30 days ago scores near 1. The decay curve is logarithmic, so recent articles are strongly favored but old articles still have a chance to surface.

  2. Duplicate clustering — The same news story published by multiple sources is clustered into a single group. The cluster algorithm uses three matching strategies in order:

    • SimHash — A 64-bit locality-sensitive hash computed from title word trigrams; articles with the same SimHash are grouped
    • URL match — If two articles share the same destination URL, they are grouped
    • Jaccard similarity — If title word overlap >= 80%, they are grouped

    Within each group, the best article surfaces (your preferred source wins; if none is marked preferred, the highest-scoring article wins). Articles from the same cluster that do not surface are excluded from the feed to prevent redundancy.

  3. Feed weighting — Each source has a weight multiplier:

    Weight Multiplier Effect
    Favorite ×1.5 Boosts score by 50%
    Normal ×1.0 Baseline
    Low ×0.6 Reduces score by 40%
    Muted ×0.0 Source excluded entirely
  4. Source cooldown — No single source is allowed to dominate. The algorithm enforces a minimum spacing (default: 4 other articles) between consecutive appearances of the same source. If violating this would leave an empty slot, the constraint relaxes to avoid gaps.

  5. Category balancing — A sliding window (size 6) ensures categories are interleaved. If two articles from the same category would appear within the window, a lower-scoring article from a different category may swap in (as long as its score is within 15% of the displaced article). This prevents any one topic from overwhelming the feed.

  6. Final ordering — Articles are sorted by their final score (recency × weight × any remaining penalty), then by publication date (newer first), then by database ID (for deterministic ordering of ties).

Priority Mode

Priority mode uses the same pipeline but squares the weight multiplier (favorite → 2.25×, normal → 1.0×, low → 0.36×). This makes favorite sources appear much more often while still respecting cooldown and category balancing.

Explainability

Every card in the Discover feed can show why it was placed where it was. The reasons are generated during ranking and stored alongside each article. Possible reasons include:

  • Recent — This article is fresher than competing articles
  • Favorite source — The source is marked as a favorite (×1.5 boost)
  • Preferred source — This source won a duplicate tiebreaker
  • Not a duplicate — A unique story, no duplicates existed
  • Spaced — Cooldown spacing was satisfied
  • Category diversity — Category was underrepresented in recent cards

YouTube & Video Playback

When an article in your feed contains a YouTube URL, FresshReader detects it and provides an inline video player experience.

Detection

The app scans article URLs for YouTube links matching any of these formats:

  • youtube.com/watch?v=VIDEO_ID
  • youtu.be/VIDEO_ID
  • youtube.com/embed/VIDEO_ID
  • m.youtube.com/watch?v=VIDEO_ID

Thumbnail

Article thumbnails are automatically set to the YouTube video's maxresdefault.jpg image (fetched directly from img.youtube.com). A play button overlay is shown over the thumbnail.

Player

When you tap the play button, the app creates an inline WebView-based player. Instead of loading YouTube's official embed (which serves ads, tracks viewing, and requires Google JavaScript), the app uses Invidious — a privacy-respecting, ad-free front-end for YouTube.

How Invidious works:

  • Invidious is an open-source proxy that fetches YouTube video streams and serves them without ads, tracking, or Google JavaScript
  • The app maintains a list of five public Invidious instances and randomly selects one for each video played (spreading load and providing redundancy):
    • yewtu.be
    • invidious.snopyta.org
    • invidious.privacydev.net
    • inv.vern.cc
    • invidious.private.coffee
  • The selected instance serves the video stream directly inside the WebView
  • No connection is made to youtube.com, googlevideo.com, doubleclick.net, or any Google domain
  • The embed page has no ads, no comments, no recommendations sidebar

What is communicated:

  • The Invidious instance receives the YouTube video ID (required to serve the video)
  • The app's IP address is visible to the Invidious instance (just as it would be to any website you visit)
  • Invidious instances are run by privacy-focused volunteers; check each instance's privacy policy for details

If the selected Invidious instance is unreachable, the player will show an error. In that case, try again — a different instance will be selected.


SponsorBlock Integration

FresshReader optionally integrates with SponsorBlock, a crowd-sourced database of sponsored segments, intros, outros, and other non-content sections in YouTube videos.

How it works:

  • When you play a video, the app fetches skip segments from https://sponsor.ajay.app/api/skipSegments?videoID=VIDEO_ID
  • If segments exist, the app injects JavaScript into the Invidious embed page that automatically skips them while you watch
  • Sponsors, intros, outros, and self-promotional segments are all skipped seamlessly

What is communicated to SponsorBlock:

  • The YouTube video ID only
  • No IP or user identification is sent (unless SponsorBlock's server logs it, as with any HTTP request)

Privacy: SponsorBlock uses video IDs as its database key. There are no user accounts, no tracking, and no analytics. The API endpoint is free and open without authentication.


Card Layout

The Discover feed uses a repeating card-size pattern for a varied, magazine-style feel:

Position (0-indexed) Card Size
0 Hero (large image, big headline)
1 Medium (standard card)
2 Medium
3 Compact (text-forward, small image)
4 Hero
5 Medium
6 Compact

The pattern repeats every 7 cards. If an article lacks an image, it gracefully steps down to the next smaller size (Hero → Medium → Compact).

Other feed modes use subsets:

  • Starred — All Hero cards (falls to Medium if no image)
  • Latest — Medium and Compact cards
  • Catch Up — Hero and Medium cards

Data & Privacy

Data Where it lives Who has access
Your FreshRSS server URL, username, password, auth token Your device, in encrypted storage (AES-256-GCM) Only you
Your feed subscriptions and articles Your device, in the local Room database; also on your FreshRSS server Only you and your FreshRSS server operator
Your read/star state Your device and your FreshRSS server Only you and your FreshRSS server operator
YouTube video IDs, when playing videos Sent to a random Invidious instance (for streaming) and to SponsorBlock (for skip segments) The Invidious instance operator; the SponsorBlock project
Article images and feed icons Fetched from original publisher servers by Coil The publisher's CDN/logs

No analytics, no crash reporting, no telemetry of any kind are included in this app. There is no Firebase, no Google Analytics, no Sentry, no bug tracker. Crash logs are written to a local file on your device and are never transmitted anywhere.


License

FresshReader is proposed for release under the Apache License 2.0.