Files
podlite/src/com/miniabs/Prefs.java
T
xockoandClaude Opus 5.5 b7e43d7d61 MiniABS 1.1: HTTPS support
- Bundle current root certificates (Let's Encrypt, GTS, Sectigo, DigiCert G2,
  Amazon, ...) missing from Android 2.x's store (Roots.java, tools/gen_roots.py)
- Ask once for unknown certificates, showing the SHA-256 fingerprint, and
  remember the answer; replaces "accept any certificate" (Trust*, Settings)
- Local streaming proxy so Android 2.x's MediaPlayer can play https:// servers,
  with seeking and a fresh token per request (StreamProxy.java)
- Tests over real TLS with good, expired, wrong-host, forged and self-signed
  certificates (test/make_pki.sh, TrustTest.java)
- README: server setup for a legacy TLS 1.0 port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:07:55 +02:00

146 lines
4.8 KiB
Java

package com.miniabs;
import java.util.UUID;
import org.json.JSONObject;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
/** Settings and sign-in state. */
final class Prefs {
static final String MODE_JWT = "jwt"; // ABS 2.26+: short access token + refresh token
static final String MODE_LEGACY = "legacy"; // older servers: one long-lived token
static final String MODE_KEY = "key"; // API key pasted by the user
private static Prefs instance;
final SharedPreferences sp;
private Prefs(Context c) {
sp = PreferenceManager.getDefaultSharedPreferences(c);
}
static synchronized Prefs get(Context c) {
if (instance == null) instance = new Prefs(c.getApplicationContext());
return instance;
}
private String s(String k) {
return sp.getString(k, "");
}
String server() { return s("server"); }
String username() { return s("username"); }
String token() { return s("access"); }
String refreshToken() { return s("refresh"); }
String mode() { return s("mode"); }
boolean wifiOnly() { return sp.getBoolean("wifi_only", true); }
boolean covers() { return sp.getBoolean("covers", true); }
String libraryId() { return s("library_id"); }
String libraryName() { return s("library_name"); }
int skipBack() { return num("skip_back", 15); }
int skipFwd() { return num("skip_fwd", 30); }
private int num(String k, int def) {
try {
return Integer.parseInt(sp.getString(k, String.valueOf(def)));
} catch (Exception e) {
return def;
}
}
boolean loggedIn() {
return server().length() > 0 && token().length() > 0;
}
boolean canRefresh() {
return MODE_JWT.equals(mode()) && refreshToken().length() > 0;
}
void setServer(String server) {
sp.edit().putString("server", server).remove("trust_all").commit();
}
synchronized void setAuth(String user, String mode, String access, String refresh) {
SharedPreferences.Editor e = sp.edit()
.putString("username", user)
.putString("mode", mode)
.putString("refresh", refresh == null ? "" : refresh);
putToken(e, access);
e.commit();
}
synchronized void setTokens(String access, String refresh) {
SharedPreferences.Editor e = sp.edit();
if (refresh != null && refresh.length() > 0) e.putString("refresh", refresh);
putToken(e, access);
e.commit();
}
/** Stores the token with its lifetime measured on this phone's clock (old phones' clocks drift). */
private void putToken(SharedPreferences.Editor e, String access) {
long lifetime = 0;
try {
String[] parts = access.split("\\.");
if (parts.length == 3) {
JSONObject p = new JSONObject(new String(Base64.decode(parts[1], Base64.URL_SAFE), "UTF-8"));
long exp = p.optLong("exp", 0), iat = p.optLong("iat", 0);
if (exp > 0 && iat > 0 && exp > iat) lifetime = exp - iat;
}
} catch (Exception ignored) {
// not a JWT, or unreadable: treat as non-expiring
}
e.putString("access", access)
.putLong("token_lifetime", lifetime)
.putLong("token_obtained", System.currentTimeMillis());
}
/** Seconds left on the access token; Long.MAX_VALUE when it doesn't expire. */
long tokenSecondsLeft() {
long life = sp.getLong("token_lifetime", 0);
if (life <= 0) return Long.MAX_VALUE;
long age = (System.currentTimeMillis() - sp.getLong("token_obtained", 0)) / 1000;
if (age < 0) return 0; // clock went backwards; be safe and refresh
return life - age;
}
long tokenObtainedAt() {
return sp.getLong("token_obtained", 0);
}
void logout() {
sp.edit().remove("access").remove("refresh").remove("mode")
.remove("library_id").remove("library_name").remove("last_episode").commit();
}
void setLibrary(String id, String name) {
sp.edit().putString("library_id", id).putString("library_name", name).commit();
}
String deviceId() {
String id = s("device_id");
if (id.length() == 0) {
id = UUID.randomUUID().toString();
sp.edit().putString("device_id", id).commit();
}
return id;
}
void setLastEpisode(Episode e) {
sp.edit().putString("last_episode", e == null ? "" : e.toJson().toString()).commit();
}
Episode lastEpisode() {
String j = s("last_episode");
if (j.length() == 0) return null;
try {
return Episode.fromStored(new JSONObject(j));
} catch (Exception e) {
return null;
}
}
}