7 Commits
Author SHA1 Message Date
XoséandClaude Opus 5.5 04ac5ae725 Podlite 1.11: install internally, keep loaded roots
Installed on the SD card, the app was killed whenever the card was shared over USB,
and a failed root read left HTTPS broken until a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:00:50 +02:00
xocko 084d7e9cad Merge pull request 'Podlite 1.10: bundled TLS 1.2, renamed from MiniABS, radio icon and amber' (#2) from bundled-tls into main 2026-10-19 17:11:36 +00:00
XoséandClaude Opus 5.5 db1175d25c Podlite 1.10: amber instead of purple, to match the radio icon
The purple came from the old Audiobookshelf icon. Accent and button gradient now take the radio's
brass and dial glow (#C27A1E, light #E8A64A, buttons #D98C2B to #A35F14), and the backgrounds,
lines and greys lose their purple tint for warm ones. The blue 'new episode' dot stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:05:22 +02:00
XoséandClaude Opus 5.5 c673a0ba5d Podlite 1.9: radio icon
New launcher icon (the Podcasts Radio pack, master in art/icon-master.png). The status-bar icon
was cut out of the old headphones artwork; make_icons.py now draws a white radio silhouette.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:57:02 +02:00
XoséandClaude Opus 5.5 042364b45c README: APK size, former name, JSON_JAR for the tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:40:21 +02:00
XoséandClaude Opus 5.5 bede72ed09 README: Newslite is now Noticias Lite
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:20:12 +02:00
xocko 10a2aa9ef5 Merge pull request 'Backport from the iOS version: never mark early endings as played, stall warning, Close' (#1) from ios-backports into main
Reviewed-on: xocko/miniabs#1
2026-10-17 14:26:41 +00:00
24 changed files with 67 additions and 53 deletions

No files matched your search

+3 -3
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.miniabs" package="com.miniabs"
android:versionCode="9" android:versionCode="12"
android:versionName="1.8" android:versionName="1.11"
android:installLocation="auto"> android:installLocation="internalOnly">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
+4 -4
View File
@@ -1,12 +1,12 @@
# Podlite # Podlite
A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java, no libraries, ~130 KB. The interface is in Galician, with large text and touch targets sized for the Mini's 240×320 screen. Its purple colour scheme and launcher icon come from the Audiobookshelf icon pack (`art/icon_master_512.png`); `tools/make_icons.py` derives the notification and player icons from it. A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java; the APK is about 1.1 MB, nearly all of it the bundled TLS stack. Formerly MiniABS. The interface is in Galician, with large text and touch targets sized for the Mini's 240×320 screen. The launcher icon is the "Podcasts Radio" pack (master in `art/icon-master.png`); `tools/make_icons.py` draws the notification icon (a white radio silhouette) and the player icons. Its warm dark colours and amber accent come from the radio: brass and the glow of its dial.
## Install ## Install
1. Copy `Podlite.apk` to the phone (USB to the SD card, or download it in the browser). 1. Copy `Podlite.apk` to the phone (USB to the SD card, or download it in the browser).
2. On the phone: **Settings → Applications → Unknown sources** → tick it. 2. On the phone: **Settings → Applications → Unknown sources** → tick it.
3. Open the APK from a file manager and install. It can be moved to the SD card (Settings → Applications → Manage applications → Podlite → Move to SD card). 3. Open the APK from a file manager and install. Podlite installs to internal storage (about 1 MB): an app on the SD card is killed whenever the phone shares its SD card over USB, which stopped playback and broke HTTPS until a restart.
## Connecting ## Connecting
@@ -64,6 +64,6 @@ Compiling against the API 8 jar guarantees no newer Android APIs are used. The A
All sizes (text, rows, buttons, covers) are in `res/values/dimens.xml`; change them there and rebuild. All sizes (text, rows, buttons, covers) are in `res/values/dimens.xml`; change them there and rebuild.
`src/com/miniabs/net/` is the bundled HTTP/TLS client shared with Plexamp Lite, Newslite and Calendar Lite; `assets/roots/` holds Mozilla's roots (regenerate with Plexamp Lite's `tools/make_roots.py`). `src/com/miniabs/net/` is the bundled HTTP/TLS client shared with Plexamp Lite, Noticias Lite and Calendar Lite; `assets/roots/` holds Mozilla's roots (regenerate with Plexamp Lite's `tools/make_roots.py`).
`test/run_tests.sh` runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`). `JSON_JAR=/path/to/json.jar test/run_tests.sh` (any org.json jar) runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"> <item android:state_pressed="true">
<shape><gradient android:startColor="#AA5CFF" android:endColor="#7022E6" android:angle="270" /><corners android:radius="6dp" /></shape> <shape><gradient android:startColor="#E39A38" android:endColor="#A9621A" android:angle="270" /><corners android:radius="6dp" /></shape>
</item> </item>
<item android:state_enabled="false"> <item android:state_enabled="false">
<shape><solid android:color="@color/dim" /><corners android:radius="6dp" /></shape> <shape><solid android:color="@color/dim" /><corners android:radius="6dp" /></shape>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"> <item android:state_pressed="true">
<shape><gradient android:startColor="#AA5CFF" android:endColor="#7022E6" android:angle="270" /><corners android:radius="23dp" /></shape> <shape><gradient android:startColor="#E39A38" android:endColor="#A9621A" android:angle="270" /><corners android:radius="23dp" /></shape>
</item> </item>
<item> <item>
<shape><gradient android:startColor="@color/accent_top" android:endColor="@color/accent_bottom" android:angle="270" /><corners android:radius="23dp" /></shape> <shape><gradient android:startColor="@color/accent_top" android:endColor="@color/accent_bottom" android:angle="270" /><corners android:radius="23dp" /></shape>
+13 -13
View File
@@ -2,20 +2,20 @@
<resources> <resources>
<string name="app_name">Podlite</string> <string name="app_name">Podlite</string>
<!-- Colours taken from the Audiobookshelf icon: gradient #9437FC to #570DCF, waves #BC8AFC --> <!-- Colours taken from the radio icon: brass #C8964F, dial glow #CC6000, on warm near-black -->
<color name="bg">#13111A</color> <color name="bg">#15120F</color>
<color name="surface">#1D1928</color> <color name="surface">#211C17</color>
<color name="header">#0C0A12</color> <color name="header">#0E0B09</color>
<color name="line">#2C2638</color> <color name="line">#342A21</color>
<color name="text">#EEEBF3</color> <color name="text">#F2EDE6</color>
<color name="sub">#ADA6BC</color> <color name="sub">#BFB4A6</color>
<color name="dim">#736C82</color> <color name="dim">#827567</color>
<color name="accent">#7A25EC</color> <!-- fills: tabs, progress, seek bar --> <color name="accent">#C27A1E</color> <!-- fills: tabs, progress, seek bar -->
<color name="accent_light">#BC8AFC</color> <!-- text and small marks on dark backgrounds --> <color name="accent_light">#E8A64A</color> <!-- text and small marks on dark backgrounds -->
<color name="accent_top">#9437FC</color> <!-- button gradient, as in the icon --> <color name="accent_top">#D98C2B</color> <!-- button gradient, as in the icon -->
<color name="accent_bottom">#5A11D3</color> <color name="accent_bottom">#A35F14</color>
<color name="fresh">#6EC1E4</color> <color name="fresh">#6EC1E4</color>
<color name="pressed">#2E2642</color> <color name="pressed">#3B2D20</color>
<style name="MiniTheme" parent="@android:style/Theme.NoTitleBar"> <style name="MiniTheme" parent="@android:style/Theme.NoTitleBar">
<item name="android:windowBackground">@color/bg</item> <item name="android:windowBackground">@color/bg</item>
+1 -1
View File
@@ -50,7 +50,7 @@
<PreferenceCategory android:title="Acerca de"> <PreferenceCategory android:title="Acerca de">
<Preference <Preference
android:key="about" android:key="about"
android:title="Podlite 1.8" android:title="Podlite 1.10"
android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" /> android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>
+1 -1
View File
@@ -26,7 +26,7 @@ import android.os.Build;
/** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */ /** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */
final class Api { final class Api {
static final String VERSION = "1.8"; static final String VERSION = "1.11";
static class ApiException extends IOException { static class ApiException extends IOException {
final int code; final int code;
+18 -6
View File
@@ -4,35 +4,47 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import android.content.Context; import android.content.Context;
/** Mozilla roots shipped as assets/roots/<sha1(subject)>-<n>.der */ /**
* Mozilla roots shipped as assets/roots/<sha1(subject)>-<n>.der
*
* Roots are read from the APK once and kept: an app installed on the SD card loses its APK while the
* card is shared over USB, and a failed read must not leave every server "untrusted" until a restart.
*/
public final class AssetRoots implements CertVerifier.RootStore { public final class AssetRoots implements CertVerifier.RootStore {
private final Context ctx; private final Context ctx;
private Set<String> names; private Set<String> names; // asset file names, once listed
private final Map<String, List<byte[]>> found = new HashMap<String, List<byte[]>>(); // by subject hash; null = none
public AssetRoots(Context c) { ctx = c.getApplicationContext(); } public AssetRoots(Context c) { ctx = c.getApplicationContext(); }
public synchronized List<byte[]> find(byte[] subjectDer) { public synchronized List<byte[]> find(byte[] subjectDer) {
String h = CertVerifier.sha1Hex(subjectDer);
if (found.containsKey(h)) return found.get(h);
try { try {
if (names == null) { if (names == null) {
names = new HashSet<String>();
String[] l = ctx.getAssets().list("roots"); String[] l = ctx.getAssets().list("roots");
if (l != null) for (int i = 0; i < l.length; i++) names.add(l[i]); if (l == null || l.length == 0) return null; // APK unreadable right now: try again next time
Set<String> n = new HashSet<String>();
for (int i = 0; i < l.length; i++) n.add(l[i]);
names = n;
} }
String h = CertVerifier.sha1Hex(subjectDer);
List<byte[]> out = null; List<byte[]> out = null;
for (int i = 0; names.contains(h + "-" + i + ".der"); i++) { for (int i = 0; names.contains(h + "-" + i + ".der"); i++) {
if (out == null) out = new ArrayList<byte[]>(); if (out == null) out = new ArrayList<byte[]>();
out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der"))); out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der")));
} }
found.put(h, out);
return out; return out;
} catch (IOException e) { } catch (IOException e) {
return null; return null; // not cached: the next handshake reads again
} }
} }
+25 -23
View File
@@ -1,9 +1,9 @@
"""Draws Podlite's PNG icons for ldpi (Galaxy Mini), mdpi and hdpi. """Draws Podlite's PNG icons for ldpi (Galaxy Mini), mdpi and hdpi.
The launcher icon (res/drawable-*/ic_launcher.png) comes from the supplied The launcher icon (res/drawable-*/ic_launcher.png) comes from the supplied
Audiobookshelf icon pack and is not generated here. The notification icon is "Podcasts Radio" icon pack (master in art/icon-master.png) and is not generated
cut out of that same artwork (art/icon_master_512.png): the white headphones here. The notification icon is a white silhouette of that radio, as Android
and equalizer become a white silhouette, as Android 2.3's status bar expects. 2.3's status bar expects.
Android 2.x has no vector drawables, so the player icons are rendered at 4x and Android 2.x has no vector drawables, so the player icons are rendered at 4x and
downsampled for smooth edges. downsampled for smooth edges.
@@ -15,7 +15,7 @@ from PIL import Image, ImageDraw
RES = sys.argv[1] RES = sys.argv[1]
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
DENS = {"ldpi": 0.75, "mdpi": 1.0, "hdpi": 1.5} DENS = {"ldpi": 0.75, "mdpi": 1.0, "hdpi": 1.5}
LILAC = (188, 138, 252, 255) # the icon's sound-wave colour (#BC8AFC) AMBER = (232, 166, 74, 255) # accent_light (#E8A64A), from the radio icon's brass
WHITE = (255, 255, 255, 255) WHITE = (255, 255, 255, 255)
SS = 4 # supersampling SS = 4 # supersampling
@@ -33,22 +33,24 @@ def save(img, px, folder, name):
def status_silhouette(): def status_silhouette():
"""White mask of the icon's light parts (headphones, bars, waves) on transparency.""" """White silhouette of the launcher icon's old radio (handle, grille, two knobs) on transparency:
src = Image.open(os.path.join(ROOT, "art", "icon_master_512.png")).convert("RGBA") Android 2.3's status bar expects a light glyph, not the colour artwork."""
out = Image.new("RGBA", src.size, (0, 0, 0, 0)) S = 24 * 32 # drawn on a 24-unit grid, supersampled
sp, op = src.load(), out.load() u = lambda v: int(round(v * 32))
for y in range(src.size[1]): m = Image.new("L", (S, S), 0)
for x in range(src.size[0]): d = ImageDraw.Draw(m)
r, g, b, a = sp[x, y] d.arc((u(6), u(1.2), u(18), u(11)), 200, 340, fill=255, width=u(1.8)) # carrying handle
# the purple background has a low green channel; the white/lilac artwork a high one d.rounded_rectangle((u(1.5), u(6.5), u(22.5), u(21.5)), radius=u(3.5), fill=255)
level = min(255, max(0, (g - 80) * 255 // 50)) d.rounded_rectangle((u(4), u(9), u(14), u(19)), radius=u(1.2), fill=0) # grille window
op[x, y] = (255, 255, 255, level * a // 255) for i in range(3): # grille bars
box = out.getbbox() y = 10.6 + i * 2.9
out = out.crop(box) d.rounded_rectangle((u(5.2), u(y), u(12.8), u(y + 1.5)), radius=u(0.7), fill=255)
side = max(out.size) for cy, r in ((11.6, 2.4), (17.0, 2.0)): # knobs as rings
sq = Image.new("RGBA", (side, side), (0, 0, 0, 0)) d.ellipse((u(18 - r), u(cy - r), u(18 + r), u(cy + r)), fill=0)
sq.paste(out, ((side - out.size[0]) // 2, (side - out.size[1]) // 2)) d.ellipse((u(18 - r + 1.1), u(cy - r + 1.1), u(18 + r - 1.1), u(cy + r - 1.1)), fill=255)
return sq out = Image.new("RGBA", (S, S), (255, 255, 255, 0))
out.putalpha(m)
return out
def status(sil, px, folder): def status(sil, px, folder):
@@ -89,9 +91,9 @@ def refresh(px, folder):
sil = status_silhouette() sil = status_silhouette()
for folder, k in DENS.items(): for folder, k in DENS.items():
status(sil, round(25 * k), folder) status(sil, round(25 * k), folder)
play(round(32 * k), folder, "ic_play", WHITE) # on the purple play button play(round(32 * k), folder, "ic_play", WHITE) # on the amber play button
pause(round(32 * k), folder, "ic_pause", WHITE) pause(round(32 * k), folder, "ic_pause", WHITE)
play(round(38 * k), folder, "ic_play_small", LILAC) # mini player bar, on dark play(round(38 * k), folder, "ic_play_small", AMBER) # mini player bar, on dark
pause(round(38 * k), folder, "ic_pause_small", LILAC) pause(round(38 * k), folder, "ic_pause_small", AMBER)
refresh(round(30 * k), folder) refresh(round(30 * k), folder)
print("icons ok") print("icons ok")