Skip to content

chrisfong/skippy

Repository files navigation

Skip Paywall

A tiny Android share-target app that reroutes any link you share through archive.is and renders the result in a built-in reader. Reader mode is the default, not an opt-in per article.

What it does

Share a link to Skip Paywall from any app. It:

  1. Rewrites the URL to https://archive.is/newest/<url>, which usually bypasses the paywall.
  2. Loads the archived page in an in-app WebView.
  3. The moment the DOM has real content, runs Mozilla's Readability.js and auto-renders a clean reader view.
  4. Toolbar actions: Show original (drop to raw archive), Open in browser (bounce the link to your chosen browser), Share.

Why reader-first

Most browsers treat reader mode as opt-in, one article at a time. If you've deliberately shared a link through an archive-bypass app, reader mode is almost always what you wanted — so it's the default, and the toggle is the escape hatch.

Install

Not on the Play Store. Build and sideload.

./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Min Android 8.0 (API 26), target SDK 34, compileSdk = 34, Kotlin 2.0, JVM target 17.

Architecture

Three activities, framework-only, no AndroidX.

Activity Purpose
ShareActivity Invisible (Theme.NoDisplay) share target filtering ACTION_SEND / text/plain. Extracts the URL via Patterns.WEB_URL, builds the archive.is/newest/<url> URL, hands to the reader.
ArchiveReaderActivity One WebView with a three-state machine (Loading / Original / Reader).
MainActivity Launcher icon; description + browser picker for the "Open in browser" toolbar action.

Reader pipeline (early-fire)

  • onPageStarted injects a small watcher script into the page. The watcher uses MutationObserver + DOMContentLoaded + short-interval polling, and fires a single callback via a @JavascriptInterface bridge the moment document.body.innerText crosses a content-length threshold and the page doesn't look like a Cloudflare challenge.
  • On bridge callback, native immediately runs Readability.parse() against several candidate subtrees — archive-is-specific containers (#CONTENT, .row), semantic tags (article, main), then the full document — and picks the candidate producing the longest article body.
  • Successful extraction: renders the article with a minimal template via loadDataWithBaseURL (system font, ~38em column, respects prefers-color-scheme).
  • Challenge / no-parse / too-short result: silently retries every 1.5s, up to 16 times. A 6s ceiling timer from onPageStarted is a belt-and-suspenders fallback in case the watcher never fires.

Constraints

  • Legacy Android framework (android.useAndroidX=false), zero third-party Gradle dependencies. Readability.js is vendored at app/src/main/assets/readability.js.
  • Kotlin sources live at app/src/main/kotlin/ (not the default src/main/java/), configured via sourceSets in app/build.gradle.kts.

Known limitations

  • archive.is periodically serves Cloudflare challenges that WebView can't auto-solve; the user interacts with them inline.
  • Some archive snapshots bury the article in structures Readability doesn't extract cleanly; extraction can fall back to the outer wrapper.
  • No font-size controls, no scroll-position persistence, no offline cache.
  • No support for archive snapshots served inside a cross-origin <iframe> (Readability can't cross frame boundaries).

Credits

  • Readability.js by Mozilla (Apache 2.0) — article extraction engine, vendored in app/src/main/assets/readability.js with upstream license header intact.
  • archive.is — the archive service this app routes through (not affiliated).

About

Skippy opens anything you share through archive.is

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors