Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions firetv/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# podcasters (p0dcasters.com) for Fire TV
# podcasters RSS reader for Fire TV

A small native Android app (`com.p0dcasters.tv`) that puts https://p0dcasters.com on a Fire TV
and makes it work with a remote. No dependencies beyond the Android SDK.
A small native Android app (`com.p0dcasters.tv`) for playing podcast episodes from HTTPS RSS
feeds that the viewer enters. It includes no podcast directory, show artwork or preloaded feeds.
The feed is fetched only after the viewer chooses Load; audio is played from the publisher's
HTTPS URL. No dependencies beyond the Android SDK.

- **WebView** on p0dcasters.com. Remote play/pause, fast-forward 30 s and rewind 15 s drive the page's player (`pitMedia`). Results open in the app; `mailto:`/`intent:` links go to the system.
- **Bundled reader UI** (`app/src/main/assets/reader.html`): a local page with no external scripts,
images or catalog requests. Native code downloads the user-selected RSS document and accepts
HTTPS audio enclosures only.
- **Remote navigation** (`app/src/main/assets/tv.js`): Chromium's WebView scrolls on arrow
keys instead of moving between links, so every D-pad press runs `pitNav(dir)` in the page:
keys instead of moving between controls, so every D-pad press runs `pitNav(dir)` in the page:
spatial focus to the nearest link/button, scrolling across long gaps, and a large focus
ring. The ring is set through `element.style` because strict CSPs (`style-src 'self'`)
blocks an injected `<style>`.
ring.
- **Typing**: OK on a text field (or Menu/Search anywhere) opens a native text box, the one
thing that reliably raises the Fire TV keyboard and its microphone. The text goes back into
the page and a search box submits.
- **Back** walks WebView history, then exits. Full-screen video and an offline screen are native.
thing that reliably raises the Fire TV keyboard and its microphone. Text fills the feed URL;
the viewer still presses Load before the app makes a request.
- **Playback**: remote play/pause, fast-forward 30 s and rewind 15 s control the selected episode.
- **Back** exits the app. Feed titles are rendered as text, and remote feed links cannot navigate
the WebView away from the bundled page.
- Leanback launcher + banner, touchscreen not required, so the Appstore lists it for Fire TV.

## Build
Expand Down Expand Up @@ -41,11 +46,10 @@ docker run --rm --device /dev/kvm -e OWNER=$(id -u):$(id -g) -v ~/scratch/androi
-v "$PWD/firetv:/w" -w /w gradle:8.7-jdk17 bash emulator-test.sh
```

Boots API 30 Android TV (the base of Fire OS 8), installs the APK, drives it with key
events (OK -> keyboard -> search, arrows through results, Back, Menu search
) and writes 1920x1080 screenshots to `store/shots/`.
Boots API 30 Android TV (the base of Fire OS 8), installs the APK and checks the blank reader,
native keyboard and HTTP URL rejection. Screenshots go under the ignored `dist/test-artifacts/`.

## Appstore

Listing copy and the art map are in `store/listing.md`; `store/render.sh` regenerates the
icons/banner from `store/src/*.svg`.
Listing copy is in `store/listing.md`; `store/render.sh` regenerates the icons/banner from
`store/src/*.svg`.
4 changes: 2 additions & 2 deletions firetv/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId 'com.p0dcasters.tv'
minSdk 22 // Fire OS 5: every Fire TV still in service
targetSdk 34
versionCode 1
versionName '1.0.0'
versionCode 2
versionName '1.1.0'
}

// The release key lives in the vault, never in the repo. scripts/build.sh
Expand Down
3 changes: 0 additions & 3 deletions firetv/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
24 changes: 0 additions & 24 deletions firetv/app/src/main/assets/offline.html

This file was deleted.

108 changes: 108 additions & 0 deletions firetv/app/src/main/assets/reader.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; media-src https: blob:; connect-src 'none'; img-src 'none'">
<title>Podcast RSS Reader</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: #14140f; color: #f4f1ea;
font: 25px/1.45 system-ui, sans-serif; }
main { max-width: 1200px; margin: 0 auto; padding: 52px 68px 80px; }
h1 { margin: 0 0 14px; color: #e27a55; font-size: 42px; }
p { max-width: 1000px; margin: 0 0 28px; color: #d0cdc5; }
form { display: flex; gap: 16px; align-items: stretch; margin: 0 0 26px; }
input, button { min-height: 68px; border: 2px solid #807b70; border-radius: 10px;
background: #24231e; color: #fff; font: inherit; padding: 12px 18px; }
input { flex: 1; min-width: 0; }
button { background: #e27a55; border-color: #e27a55; color: #14140f; font-weight: 700; }
:focus { outline: 4px solid #e27a55; outline-offset: 4px; }
#status { min-height: 1.5em; color: #d0cdc5; }
#feed-title { margin: 34px 0 12px; font-size: 30px; }
#episodes { display: grid; gap: 12px; }
.episode { display: flex; align-items: center; gap: 20px; padding: 16px;
border: 1px solid #4a4840; border-radius: 10px; }
.episode-title { flex: 1; overflow-wrap: anywhere; }
audio { width: 100%; margin-top: 24px; }
.empty { color: #9e9b92; }
</style>
</head>
<body>
<main>
<h1>Podcast RSS Reader</h1>
<p>Enter an HTTPS podcast RSS feed URL you choose. This app has no podcast catalog, show artwork, or preloaded episodes.</p>
<form id="feed-form">
<input id="feed-url" name="q" type="url" inputmode="url" autocomplete="url"
placeholder="https://example.com/podcast.rss" aria-label="Podcast RSS feed URL" autofocus>
<button type="submit">Load feed</button>
</form>
<p id="status" role="status" aria-live="polite">No feed loaded. Add a feed URL to begin.</p>
<h2 id="feed-title"></h2>
<section id="episodes" aria-label="Episodes"></section>
<audio id="player" controls hidden></audio>
</main>
<script>
(function () {
var form = document.getElementById('feed-form');
var input = document.getElementById('feed-url');
var status = document.getElementById('status');
var title = document.getElementById('feed-title');
var episodes = document.getElementById('episodes');
var player = document.getElementById('player');

window.setFeedUrl = function (value) {
input.value = value || '';
input.focus();
};

window.feedLoaded = function (result) {
episodes.textContent = '';
title.textContent = '';
player.pause();
player.removeAttribute('src');
player.hidden = true;
if (result.error) {
status.textContent = result.error;
return;
}
title.textContent = result.title;
status.textContent = 'Choose an episode to play.';
result.episodes.forEach(function (episode) {
var row = document.createElement('div');
row.className = 'episode';
var name = document.createElement('span');
name.className = 'episode-title';
name.textContent = episode.title;
var play = document.createElement('button');
play.type = 'button';
play.textContent = 'Play';
play.addEventListener('click', function () {
player.src = episode.audio;
player.hidden = false;
player.play();
status.textContent = 'Playing: ' + episode.title;
});
row.appendChild(name);
row.appendChild(play);
episodes.appendChild(row);
});
};

form.addEventListener('submit', function (event) {
event.preventDefault();
var address = input.value.trim();
if (!/^https:\/\//i.test(address)) {
status.textContent = 'Enter an HTTPS feed URL.';
return;
}
episodes.textContent = '';
title.textContent = '';
status.textContent = 'Loading the feed you selected…';
window.Podcasters.loadFeed(address);
});
})();
</script>
</body>
</html>
8 changes: 3 additions & 5 deletions firetv/app/src/main/assets/tv.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Podcasters (p0dcasters.com) for Fire TV: remote-control navigation for any page in the WebView.
/* Podcasters RSS reader for Fire TV: remote-control navigation for the bundled reader UI.
*
* Chromium's WebView scrolls on arrow keys instead of moving between links, so
* the activity hands every D-pad press to pitNav() and OK to pitEnter(). Both
Expand All @@ -14,9 +14,7 @@
var TEXT = /^(search|text|email|url|tel|number|password|)$/;
var field = null; // the text field OK was pressed on, while the keyboard is up

// The focus ring is set through CSSOM on the element itself: p0dcasters.com (and
// many sites) ship style-src 'self', which blocks an injected <style> but not
// element.style. A TV needs a ring you can see from the couch.
// A TV needs a focus ring that can be seen from the couch.
var RING = [['outline', '4px solid #e27a55'], ['outline-offset', '3px'],
['box-shadow', '0 0 0 8px rgba(5,5,5,.85)']];
function ring(el, on) {
Expand Down Expand Up @@ -148,7 +146,7 @@
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
var type = (el.getAttribute('type') || '').toLowerCase();
if (el.form && (type === 'search' || el.name === 'q')) {
if (el.form && type === 'search') {
if (el.form.requestSubmit) el.form.requestSubmit(); else el.form.submit();
return 'submitted';
}
Expand Down
Loading
Loading