Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04ac5ae725 | ||
|
|
084d7e9cad | ||
|
|
db1175d25c | ||
|
|
c673a0ba5d | ||
|
|
042364b45c | ||
|
|
bede72ed09 | ||
|
|
a4be0bab44 | ||
|
|
10a2aa9ef5 |
No files matched your search
@@ -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="8"
|
android:versionCode="12"
|
||||||
android:versionName="1.7"
|
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" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
# MiniABS
|
# 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 `MiniABS.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 → MiniABS → 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
|
||||||
|
|
||||||
At home, the simplest option is your server's **local http:// address**, e.g. `http://192.168.1.10:13378`.
|
At home, the simplest option is your server's **local http:// address**, e.g. `http://192.168.1.10:13378`.
|
||||||
|
|
||||||
Sign in with your username and password, or tick "Use an API key instead". It works with both current servers (2.26+, short-lived access tokens that MiniABS refreshes automatically) and older ones (single long-lived token).
|
Sign in with your username and password, or tick "Use an API key instead". It works with both current servers (2.26+, short-lived access tokens that Podlite refreshes automatically) and older ones (single long-lived token).
|
||||||
|
|
||||||
## HTTPS (from outside your home)
|
## HTTPS (from outside your home)
|
||||||
|
|
||||||
Android 2.3 only speaks TLS 1.0, which modern servers refuse, so since 1.7 MiniABS brings its own
|
Android 2.3 only speaks TLS 1.0, which modern servers refuse, so since 1.7 Podlite brings its own
|
||||||
**TLS 1.2** (Bouncy Castle, the same client as Minamp, with Mozilla's root certificates,
|
**TLS 1.2** (Bouncy Castle, the same client as Plexamp Lite, with Mozilla's root certificates,
|
||||||
keep-alive and session resumption). Sign in with your server's normal `https://` address, at
|
keep-alive and session resumption). Sign in with your server's normal `https://` address, at
|
||||||
home or away; nothing changes on the server. (A legacy TLS 1.0 port set up for older versions
|
home or away; nothing changes on the server. (A legacy TLS 1.0 port set up for older versions
|
||||||
keeps working too.)
|
keeps working too.)
|
||||||
|
|
||||||
### App: certificates
|
### App: certificates
|
||||||
|
|
||||||
- **Built-in roots:** MiniABS checks certificates against Mozilla's current root list (`assets/roots`), so Let's Encrypt, Google Trust Services, Sectigo/ZeroSSL and the rest just work.
|
- **Built-in roots:** Podlite checks certificates against Mozilla's current root list (`assets/roots`), so Let's Encrypt, Google Trust Services, Sectigo/ZeroSSL and the rest just work.
|
||||||
- **Anything else** (self-signed, a private CA, a new root): MiniABS shows the certificate's SHA-256 fingerprint and asks once. Compare it with the fingerprint your computer's browser shows for the site before tapping Trust. It then trusts the CA that issued it, so renewals keep working.
|
- **Anything else** (self-signed, a private CA, a new root): Podlite shows the certificate's SHA-256 fingerprint and asks once. Compare it with the fingerprint your computer's browser shows for the site before tapping Trust. It then trusts the CA that issued it, so renewals keep working.
|
||||||
- **Settings → Certificates you trusted** lets you forget them.
|
- **Settings → Certificates you trusted** lets you forget them.
|
||||||
|
|
||||||
### App: streaming
|
### App: streaming
|
||||||
|
|
||||||
Android 2.x's media player can't stream modern https:// URLs, so for HTTPS servers MiniABS runs a small proxy on the phone (127.0.0.1 only, random unguessable URLs). It forwards seeks and gets a fresh sign-in token for each request, so long episodes don't break when the token expires.
|
Android 2.x's media player can't stream modern https:// URLs, so for HTTPS servers Podlite runs a small proxy on the phone (127.0.0.1 only, random unguessable URLs). It forwards seeks and gets a fresh sign-in token for each request, so long episodes don't break when the token expires.
|
||||||
|
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
@@ -38,15 +38,15 @@ Android 2.x's media player can't stream modern https:// URLs, so for HTTPS serve
|
|||||||
- **Shows**: every podcast; tap one for its episodes. Menu key → sort oldest/newest, hide played.
|
- **Shows**: every podcast; tap one for its episodes. Menu key → sort oldest/newest, hide played.
|
||||||
- **Saved**: episodes downloaded to the SD card, plus download progress.
|
- **Saved**: episodes downloaded to the SD card, plus download progress.
|
||||||
- **Tap** an episode to play. **Long-press** for Download, Delete download, Mark as played/unplayed, Go to show.
|
- **Tap** an episode to play. **Long-press** for Download, Delete download, Mark as played/unplayed, Go to show.
|
||||||
- **Player**: skip back/forward (set the amounts in Settings), seek bar, show notes. Menu key → sleep timer, mark as played, download, stop, close MiniABS.
|
- **Player**: skip back/forward (set the amounts in Settings), seek bar, show notes. Menu key → sleep timer, mark as played, download, stop, close Podlite.
|
||||||
- **Close**: Menu key → *Pechar MiniABS* (on the Latest/Shows/Saved screen or the player) saves the position, stops playback and closes the app.
|
- **Close**: Menu key → *Pechar Podlite* (on the Latest/Shows/Saved screen or the player) saves the position, stops playback and closes the app.
|
||||||
- **Headset buttons**: play/pause; next/previous skip forward/back. Playback pauses when headphones are unplugged and during calls.
|
- **Headset buttons**: play/pause; next/previous skip forward/back. Playback pauses when headphones are unplugged and during calls.
|
||||||
|
|
||||||
Colored dots: blue = new, amber = in progress (with a bar), grey = played.
|
Colored dots: blue = new, amber = in progress (with a bar), grey = played.
|
||||||
|
|
||||||
## Progress sync
|
## Progress sync
|
||||||
|
|
||||||
Playback opens an Audiobookshelf session and syncs every 15 seconds and on pause, so progress follows you to other apps. An episode only counts as played when playback gets within a minute of its end: a stream that drops or a saved file cut short is retried or reported, never marked as played. If playback says it's playing but stops moving for 12 seconds, MiniABS says so. Saved episodes play offline; their positions are kept on the phone and sent to the server next time you're online (or tap refresh on the Saved tab).
|
Playback opens an Audiobookshelf session and syncs every 15 seconds and on pause, so progress follows you to other apps. An episode only counts as played when playback gets within a minute of its end: a stream that drops or a saved file cut short is retried or reported, never marked as played. If playback says it's playing but stops moving for 12 seconds, Podlite says so. Saved episodes play offline; their positions are kept on the phone and sent to the server next time you're online (or tap refresh on the Saved tab).
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ Playback opens an Audiobookshelf session and syncs every 15 seconds and on pause
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Needs a JDK, `aapt`, `dalvik-exchange` (dx), `zipalign`, `apksigner` (Ubuntu: `apt install aapt dalvik-exchange zipalign apksigner`), an API 8 `android.jar`, and the renamed Bouncy Castle sources from Minamp's `tools/setup_toolchain.sh` (`BC_SRC`, default `~/android-toolchain/bcsrc`):
|
Needs a JDK, `aapt`, `dalvik-exchange` (dx), `zipalign`, `apksigner` (Ubuntu: `apt install aapt dalvik-exchange zipalign apksigner`), an API 8 `android.jar`, and the renamed Bouncy Castle sources from Plexamp Lite's `tools/setup_toolchain.sh` (`BC_SRC`, default `~/android-toolchain/bcsrc`):
|
||||||
|
|
||||||
ANDROID_JAR=/path/to/android-8/android.jar ./build.sh
|
ANDROID_JAR=/path/to/android-8/android.jar ./build.sh
|
||||||
|
|
||||||
@@ -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 Minamp, FreshMini and GingerCal; `assets/roots/` holds Mozilla's roots (regenerate with Minamp'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`).
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 224 KiB |
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Builds MiniABS.apk for Android 2.2+ (API 8) using the plain SDK tools, no Gradle.
|
# Builds Podlite.apk for Android 2.2+ (API 8) using the plain SDK tools, no Gradle.
|
||||||
# ANDROID_JAR android.jar for API 8 (compiling against it guarantees no newer APIs sneak in)
|
# ANDROID_JAR android.jar for API 8 (compiling against it guarantees no newer APIs sneak in)
|
||||||
# KEYSTORE signing keystore (created on first build if missing)
|
# KEYSTORE signing keystore (created on first build if missing)
|
||||||
set -e
|
set -e
|
||||||
@@ -14,7 +14,7 @@ mkdir -p "$OUT/gen" "$OUT/classes"
|
|||||||
|
|
||||||
echo "== resources"
|
echo "== resources"
|
||||||
aapt package -f -m -J "$OUT/gen" -M AndroidManifest.xml -S res -A assets -I "$ANDROID_JAR" \
|
aapt package -f -m -J "$OUT/gen" -M AndroidManifest.xml -S res -A assets -I "$ANDROID_JAR" \
|
||||||
-F "$OUT/MiniABS.unsigned.apk"
|
-F "$OUT/Podlite.unsigned.apk"
|
||||||
|
|
||||||
echo "== compile (Java 8 bytecode, API 8 class library)"
|
echo "== compile (Java 8 bytecode, API 8 class library)"
|
||||||
javac -nowarn -Xlint:-options -encoding UTF-8 -source 8 -target 8 \
|
javac -nowarn -Xlint:-options -encoding UTF-8 -source 8 -target 8 \
|
||||||
@@ -23,16 +23,16 @@ javac -nowarn -Xlint:-options -encoding UTF-8 -source 8 -target 8 \
|
|||||||
|
|
||||||
echo "== dex"
|
echo "== dex"
|
||||||
dalvik-exchange --dex --min-sdk-version=8 --output="$OUT/classes.dex" "$OUT/classes"
|
dalvik-exchange --dex --min-sdk-version=8 --output="$OUT/classes.dex" "$OUT/classes"
|
||||||
(cd "$OUT" && aapt add MiniABS.unsigned.apk classes.dex > /dev/null)
|
(cd "$OUT" && aapt add Podlite.unsigned.apk classes.dex > /dev/null)
|
||||||
|
|
||||||
echo "== align + sign (v1 JAR signature with SHA-1, which Android 2.x requires)"
|
echo "== align + sign (v1 JAR signature with SHA-1, which Android 2.x requires)"
|
||||||
if [ ! -f "$KEYSTORE" ]; then
|
if [ ! -f "$KEYSTORE" ]; then
|
||||||
keytool -genkeypair -keystore "$KEYSTORE" -storepass miniabs -keypass miniabs -alias miniabs \
|
keytool -genkeypair -keystore "$KEYSTORE" -storepass miniabs -keypass miniabs -alias miniabs \
|
||||||
-keyalg RSA -keysize 2048 -validity 10000 -dname "CN=MiniABS" > /dev/null 2>&1
|
-keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Podlite" > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
zipalign -f 4 "$OUT/MiniABS.unsigned.apk" "$OUT/MiniABS.aligned.apk"
|
zipalign -f 4 "$OUT/Podlite.unsigned.apk" "$OUT/Podlite.aligned.apk"
|
||||||
apksigner sign --ks "$KEYSTORE" --ks-pass pass:miniabs --key-pass pass:miniabs --ks-key-alias miniabs \
|
apksigner sign --ks "$KEYSTORE" --ks-pass pass:miniabs --key-pass pass:miniabs --ks-key-alias miniabs \
|
||||||
--min-sdk-version 8 --v1-signing-enabled true --v2-signing-enabled false --v3-signing-enabled false \
|
--min-sdk-version 8 --v1-signing-enabled true --v2-signing-enabled false --v3-signing-enabled false \
|
||||||
--out "$OUT/MiniABS.apk" "$OUT/MiniABS.aligned.apk"
|
--out "$OUT/Podlite.apk" "$OUT/Podlite.aligned.apk"
|
||||||
apksigner verify --min-sdk-version 8 "$OUT/MiniABS.apk"
|
apksigner verify --min-sdk-version 8 "$OUT/Podlite.apk"
|
||||||
ls -l "$OUT/MiniABS.apk"
|
ls -l "$OUT/Podlite.apk"
|
||||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 977 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 453 B |
|
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 499 B |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 863 B |
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 615 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 19 KiB |
@@ -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,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>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="MiniABS"
|
android:text="Podlite"
|
||||||
android:textSize="@dimen/header_text"
|
android:textSize="@dimen/header_text"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/accent_light" />
|
android:textColor="@color/accent_light" />
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="MiniABS"
|
android:text="Podlite"
|
||||||
android:textSize="@dimen/login_title"
|
android:textSize="@dimen/login_title"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/accent_light" />
|
android:textColor="@color/accent_light" />
|
||||||
@@ -99,6 +99,6 @@
|
|||||||
android:textSize="@dimen/login_tip"
|
android:textSize="@dimen/login_tip"
|
||||||
android:textColor="@color/dim"
|
android:textColor="@color/dim"
|
||||||
android:lineSpacingExtra="2dp"
|
android:lineSpacingExtra="2dp"
|
||||||
android:text="Usa o enderezo https:// normal do servidor, na casa ou fóra: MiniABS trae o seu propio TLS moderno. Na casa tamén vale o enderezo local http://. Se o certificado non se recoñece, amosaráseche a súa pegada dixital para que a comprobes." />
|
android:text="Usa o enderezo https:// normal do servidor, na casa ou fóra: Podlite trae o seu propio TLS moderno. Na casa tamén vale o enderezo local http://. Se o certificado non se recoñece, amosaráseche a súa pegada dixital para que a comprobes." />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
Every size in MiniABS lives here. Tuned for the Galaxy Mini (240x320, 120 dpi), where
|
Every size in Podlite lives here. Tuned for the Galaxy Mini (240x320, 120 dpi), where
|
||||||
1dp = 0.75 px: 16sp text is 12 px tall, and 48dp touch targets are 36 px (about 7 mm).
|
1dp = 0.75 px: 16sp text is 12 px tall, and 48dp touch targets are 36 px (about 7 mm).
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MiniABS</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>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<PreferenceCategory android:title="Acerca de">
|
<PreferenceCategory android:title="Acerca de">
|
||||||
<Preference
|
<Preference
|
||||||
android:key="about"
|
android:key="about"
|
||||||
android:title="MiniABS 1.5"
|
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>
|
||||||
@@ -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.7";
|
static final String VERSION = "1.11";
|
||||||
|
|
||||||
static class ApiException extends IOException {
|
static class ApiException extends IOException {
|
||||||
final int code;
|
final int code;
|
||||||
@@ -64,7 +64,7 @@ final class Api {
|
|||||||
conn.setUseCaches(false);
|
conn.setUseCaches(false);
|
||||||
conn.setInstanceFollowRedirects(true);
|
conn.setInstanceFollowRedirects(true);
|
||||||
setMethod(conn, method);
|
setMethod(conn, method);
|
||||||
conn.setRequestProperty("User-Agent", "MiniABS/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
|
conn.setRequestProperty("User-Agent", "Podlite/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ final class Api {
|
|||||||
private static JSONObject deviceInfo(Context c) throws JSONException {
|
private static JSONObject deviceInfo(Context c) throws JSONException {
|
||||||
return new JSONObject()
|
return new JSONObject()
|
||||||
.put("deviceId", Prefs.get(c).deviceId())
|
.put("deviceId", Prefs.get(c).deviceId())
|
||||||
.put("clientName", "MiniABS")
|
.put("clientName", "Podlite")
|
||||||
.put("clientVersion", VERSION)
|
.put("clientVersion", VERSION)
|
||||||
.put("manufacturer", Build.MANUFACTURER)
|
.put("manufacturer", Build.MANUFACTURER)
|
||||||
.put("model", Build.MODEL)
|
.put("model", Build.MODEL)
|
||||||
@@ -416,7 +416,7 @@ final class Api {
|
|||||||
.put("forceDirectPlay", true)
|
.put("forceDirectPlay", true)
|
||||||
.put("forceTranscode", false)
|
.put("forceTranscode", false)
|
||||||
.put("supportedMimeTypes", mimes)
|
.put("supportedMimeTypes", mimes)
|
||||||
.put("mediaPlayer", "MiniABS");
|
.put("mediaPlayer", "Podlite");
|
||||||
} catch (JSONException ignored) {
|
} catch (JSONException ignored) {
|
||||||
}
|
}
|
||||||
return obj(call(c, "POST", "/api/items/" + e.itemId + "/play/" + e.id, body, 200,
|
return obj(call(c, "POST", "/api/items/" + e.itemId + "/play/" + e.id, body, 200,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class DownloadService extends IntentService {
|
|||||||
private static volatile String cancelId;
|
private static volatile String cancelId;
|
||||||
|
|
||||||
public DownloadService() {
|
public DownloadService() {
|
||||||
super("MiniABS downloads");
|
super("Podlite downloads");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void enqueue(Context c, Episode e) {
|
static void enqueue(Context c, Episode e) {
|
||||||
@@ -105,9 +105,9 @@ public class DownloadService extends IntentService {
|
|||||||
percent = 0;
|
percent = 0;
|
||||||
cancelId = null;
|
cancelId = null;
|
||||||
PowerManager.WakeLock wake = ((PowerManager) getSystemService(POWER_SERVICE))
|
PowerManager.WakeLock wake = ((PowerManager) getSystemService(POWER_SERVICE))
|
||||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MiniABS download");
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Podlite download");
|
||||||
WifiManager.WifiLock wifi = ((WifiManager) getSystemService(WIFI_SERVICE))
|
WifiManager.WifiLock wifi = ((WifiManager) getSystemService(WIFI_SERVICE))
|
||||||
.createWifiLock(WifiManager.WIFI_MODE_FULL, "MiniABS download");
|
.createWifiLock(WifiManager.WIFI_MODE_FULL, "Podlite download");
|
||||||
wake.acquire();
|
wake.acquire();
|
||||||
wifi.acquire();
|
wifi.acquire();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.miniabs;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/** One podcast episode, trimmed down to what MiniABS needs. */
|
/** One podcast episode, trimmed down to what Podlite needs. */
|
||||||
final class Episode {
|
final class Episode {
|
||||||
String itemId = ""; // library item (the podcast) id
|
String itemId = ""; // library item (the podcast) id
|
||||||
String id = ""; // episode id
|
String id = ""; // episode id
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class HomeActivity extends BaseActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
if (intent.getBooleanExtra("exit", false)) { closeApp(); return; } // "Pechar MiniABS" from the player
|
if (intent.getBooleanExtra("exit", false)) { closeApp(); return; } // "Pechar Podlite" from the player
|
||||||
if (intent.hasExtra("tab")) selectTab(intent.getIntExtra("tab", TAB_LATEST));
|
if (intent.hasExtra("tab")) selectTab(intent.getIntExtra("tab", TAB_LATEST));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ public class HomeActivity extends BaseActivity {
|
|||||||
menu.add(0, 2, 0, "Biblioteca").setIcon(android.R.drawable.ic_menu_agenda);
|
menu.add(0, 2, 0, "Biblioteca").setIcon(android.R.drawable.ic_menu_agenda);
|
||||||
menu.add(0, 3, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
|
menu.add(0, 3, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
|
||||||
menu.add(0, 4, 0, "Axustes").setIcon(android.R.drawable.ic_menu_preferences);
|
menu.add(0, 4, 0, "Axustes").setIcon(android.R.drawable.ic_menu_preferences);
|
||||||
menu.add(0, 5, 0, "Pechar MiniABS").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
|
menu.add(0, 5, 0, "Pechar Podlite").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.Map;
|
|||||||
import com.miniabs.net.Http;
|
import com.miniabs.net.Http;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HttpURLConnection backed by the bundled TLS 1.2 client (com.miniabs.net, from Minamp), so
|
* An HttpURLConnection backed by the bundled TLS 1.2 client (com.miniabs.net, from Plexamp Lite), so
|
||||||
* the existing API, cover, download and stream code keeps its shape while the phone talks to the
|
* the existing API, cover, download and stream code keeps its shape while the phone talks to the
|
||||||
* normal https:// address: Android 2.x's own TLS 1.0 can't. Any method works (PATCH too), and
|
* normal https:// address: Android 2.x's own TLS 1.0 can't. Any method works (PATCH too), and
|
||||||
* connections are kept alive between requests.
|
* connections are kept alive between requests.
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ public class PlayerActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
menu.add(0, 4, 0, "Ir ao programa").setIcon(android.R.drawable.ic_menu_view);
|
menu.add(0, 4, 0, "Ir ao programa").setIcon(android.R.drawable.ic_menu_view);
|
||||||
if (active) menu.add(0, 5, 0, "Deter").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
|
if (active) menu.add(0, 5, 0, "Deter").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
|
||||||
menu.add(0, 6, 0, "Pechar MiniABS").setIcon(android.R.drawable.ic_lock_power_off);
|
menu.add(0, 6, 0, "Pechar Podlite").setIcon(android.R.drawable.ic_lock_power_off);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
instance = this;
|
instance = this;
|
||||||
am = (AudioManager) getSystemService(AUDIO_SERVICE);
|
am = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||||
wifi = ((WifiManager) getSystemService(WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "MiniABS");
|
wifi = ((WifiManager) getSystemService(WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "Podlite");
|
||||||
wifi.setReferenceCounted(false);
|
wifi.setReferenceCounted(false);
|
||||||
buttons = new ComponentName(this, MediaButtonReceiver.class);
|
buttons = new ComponentName(this, MediaButtonReceiver.class);
|
||||||
registerReceiver(noisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
registerReceiver(noisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets Android's media player stream from an HTTPS server. The player can't do HTTPS before
|
* Lets Android's media player stream from an HTTPS server. The player can't do HTTPS before
|
||||||
* Android 3.1 and wouldn't use MiniABS's certificate trust anyway, so it plays
|
* Android 3.1 and wouldn't use Podlite's certificate trust anyway, so it plays
|
||||||
* http://127.0.0.1:PORT/<random id> and this proxy fetches the real stream (forwarding seeks as
|
* http://127.0.0.1:PORT/<random id> and this proxy fetches the real stream (forwarding seeks as
|
||||||
* Range requests) with a fresh sign-in token each time.
|
* Range requests) with a fresh sign-in token each time.
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +37,7 @@ final class StreamProxy implements Runnable {
|
|||||||
private StreamProxy(Context c) throws IOException {
|
private StreamProxy(Context c) throws IOException {
|
||||||
this.c = c.getApplicationContext();
|
this.c = c.getApplicationContext();
|
||||||
server = new ServerSocket(0, 8, InetAddress.getByName("127.0.0.1"));
|
server = new ServerSocket(0, 8, InetAddress.getByName("127.0.0.1"));
|
||||||
Thread t = com.miniabs.net.Threads.create(this, "MiniABS proxy");
|
Thread t = com.miniabs.net.Threads.create(this, "Podlite proxy");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ final class StreamProxy implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
serve(s);
|
serve(s);
|
||||||
}
|
}
|
||||||
}, "MiniABS proxy conn");
|
}, "Podlite proxy conn");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import com.miniabs.net.AssetRoots;
|
|||||||
import com.miniabs.net.CertVerifier;
|
import com.miniabs.net.CertVerifier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decides which HTTPS certificates MiniABS accepts, for the bundled TLS 1.2 client:
|
* Decides which HTTPS certificates Podlite accepts, for the bundled TLS 1.2 client:
|
||||||
* 1. certificates chaining to Mozilla's roots (assets/roots; Android 2.3's own list predates Let's Encrypt);
|
* 1. certificates chaining to Mozilla's roots (assets/roots; Android 2.3's own list predates Let's Encrypt);
|
||||||
* 2. certificates the user chose to trust after checking the fingerprint ("pins").
|
* 2. certificates the user chose to trust after checking the fingerprint ("pins").
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import com.minamp.bc.crypto.util.PublicKeyFactory;
|
|||||||
* Minimal Web PKI path validation for the bundled TLS stack:
|
* Minimal Web PKI path validation for the bundled TLS stack:
|
||||||
* hostname match, validity window, signature chain up to a bundled Mozilla root.
|
* hostname match, validity window, signature chain up to a bundled Mozilla root.
|
||||||
* Optionally one pinned leaf certificate (SHA-256) is trusted as-is, for self-signed servers.
|
* Optionally one pinned leaf certificate (SHA-256) is trusted as-is, for self-signed servers.
|
||||||
* Shared with Minamp (tools/tlskit in the iOS ports' history); keep the copies in sync.
|
* Shared with Plexamp Lite (tools/tlskit in the iOS ports' history); keep the copies in sync.
|
||||||
*/
|
*/
|
||||||
public final class CertVerifier {
|
public final class CertVerifier {
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ import com.minamp.bc.tls.crypto.impl.bc.BcTlsCrypto;
|
|||||||
/**
|
/**
|
||||||
* Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and
|
* Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and
|
||||||
* session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots,
|
* session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots,
|
||||||
* so every request in the app goes through here. From Minamp; keep the copies in sync.
|
* so every request in the app goes through here. From Plexamp Lite; keep the copies in sync.
|
||||||
*/
|
*/
|
||||||
public final class Http {
|
public final class Http {
|
||||||
|
|
||||||
public static String USER_AGENT = "Minamp/1.0";
|
public static String USER_AGENT = "PlexampLite/1.0";
|
||||||
|
|
||||||
/** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */
|
/** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */
|
||||||
public static String proxyHost;
|
public static String proxyHost;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
/** Runs MiniABS's real API, JSON and progress code against mock_abs.py on the desktop JVM. */
|
/** Runs Podlite's real API, JSON and progress code against mock_abs.py on the desktop JVM. */
|
||||||
public class ApiTest {
|
public class ApiTest {
|
||||||
static int pass, fail;
|
static int pass, fail;
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ public class ApiTest {
|
|||||||
if (path.startsWith("/api/items/pod1/play/")) play = r;
|
if (path.startsWith("/api/items/pod1/play/")) play = r;
|
||||||
}
|
}
|
||||||
check(play != null && play.getJSONObject("b").getBoolean("forceDirectPlay")
|
check(play != null && play.getJSONObject("b").getBoolean("forceDirectPlay")
|
||||||
&& play.getJSONObject("b").getJSONObject("deviceInfo").getString("clientName").equals("MiniABS"),
|
&& play.getJSONObject("b").getJSONObject("deviceInfo").getString("clientName").equals("Podlite"),
|
||||||
"play request asks for direct play and names the client");
|
"play request asks for direct play and names the client");
|
||||||
check(sync != null && sync.getJSONObject("b").getDouble("currentTime") == 615.5
|
check(sync != null && sync.getJSONObject("b").getDouble("currentTime") == 615.5
|
||||||
&& sync.getJSONObject("b").getDouble("timeListened") == 15.2, "progress sync sent");
|
&& sync.getJSONObject("b").getDouble("timeListened") == 15.2, "progress sync sent");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Runs MiniABS's API/JSON/progress/HTTPS code on the desktop JVM against mock_abs.py.
|
# Runs Podlite's API/JSON/progress/HTTPS code on the desktop JVM against mock_abs.py.
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
OUT=build/test
|
OUT=build/test
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Draws MiniABS'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")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""One-off: translates MiniABS's interface strings to Galician in place.
|
"""One-off: translates Podlite's interface strings to Galician in place.
|
||||||
Each (file, old, new) must match at least once, so nothing is silently missed."""
|
Each (file, old, new) must match at least once, so nothing is silently missed."""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|||||||