- 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>
146 lines
4.8 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|