Import miniabs 1.0 source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4fe9eed870
78 files changed
+5800
No files matched your search
@@ -0,0 +1,215 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/** Runs MiniABS's real API, JSON and progress code against mock_abs.py on the desktop JVM. */
|
||||
public class ApiTest {
|
||||
static int pass, fail;
|
||||
|
||||
static void check(boolean ok, String what) {
|
||||
if (ok) pass++;
|
||||
else fail++;
|
||||
System.out.println((ok ? " PASS " : " FAIL ") + what);
|
||||
}
|
||||
|
||||
static Object server(String base, String path) throws Exception {
|
||||
HttpURLConnection c = (HttpURLConnection) new URL(base + path).openConnection();
|
||||
Object o = Json.parse(new InputStreamReader(c.getInputStream(), "UTF-8"), 100000, null);
|
||||
c.disconnect();
|
||||
return o;
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
String base = "http://127.0.0.1:" + a[0];
|
||||
boolean legacy = a.length > 1 && a[1].equals("legacy");
|
||||
Context c = new Context();
|
||||
Prefs p = Prefs.get(c);
|
||||
|
||||
System.out.println("JSON parser");
|
||||
Object j = Json.parse(new StringReader(
|
||||
"{\"a\":[1,-2.5e3,true,false,null,\"caf\\u00e9 \\ud83c\\udfa7 \\\"q\\\" \\\\ /\"],\"big\":12345678901,"
|
||||
+ "\"chapters\":[{\"x\":[1,2,{\"y\":\"}]\"}]}],\"n\":{}}"), 50, null);
|
||||
JSONObject jo = (JSONObject) j;
|
||||
JSONArray arr = jo.getJSONArray("a");
|
||||
check(arr.getInt(0) == 1 && arr.getDouble(1) == -2500.0 && arr.getBoolean(2) && !arr.getBoolean(3)
|
||||
&& arr.isNull(4), "numbers, booleans and null");
|
||||
check(arr.getString(5).equals("café 🎧 \"q\" \\ /"), "string escapes incl. emoji surrogates");
|
||||
check(jo.getLong("big") == 12345678901L, "long numbers");
|
||||
check(!jo.has("chapters") && jo.has("n"), "bulky keys skipped, parsing resumes correctly after them");
|
||||
JSONObject cut = (JSONObject) Json.parse(new StringReader("{\"d\":\"0123456789abcdef\"}"), 10, null);
|
||||
check(cut.getString("d").equals("0123456789…"), "long strings truncated with an ellipsis");
|
||||
|
||||
System.out.println("Sign-in");
|
||||
p.setServer(LoginActivity_normalize(base + "/login/"), false);
|
||||
check(p.server().equals(base), "address normalised (" + p.server() + ")");
|
||||
check(Api.status(c, p.server()).equals("2.30.0"), "status check finds the server");
|
||||
try {
|
||||
Api.login(c, p.server(), "demo", "wrong");
|
||||
check(false, "wrong password rejected");
|
||||
} catch (Api.ApiException e) {
|
||||
check(e.code == 401 && e.getMessage().contains("Wrong"), "wrong password rejected with a clear message");
|
||||
}
|
||||
String lib = Api.login(c, p.server(), "demo", "pw");
|
||||
check(lib.equals("lib-pod"), "login returns default library");
|
||||
if (legacy) {
|
||||
check(Prefs.MODE_LEGACY.equals(p.mode()) && !p.canRefresh(), "old server: legacy token mode");
|
||||
check(p.tokenSecondsLeft() > 3500, "legacy token lifetime read");
|
||||
} else {
|
||||
check(Prefs.MODE_JWT.equals(p.mode()) && p.canRefresh(), "new server: access + refresh token mode");
|
||||
long left = p.tokenSecondsLeft();
|
||||
check(left > 3590 && left <= 3600, "token lifetime read from JWT (" + left + "s)");
|
||||
}
|
||||
|
||||
System.out.println("Browsing");
|
||||
JSONArray libs = Api.libraries(c);
|
||||
check(libs.length() == 2, "libraries listed");
|
||||
JSONObject recent = Api.recentEpisodes(c, "lib-pod", 0, 20);
|
||||
JSONArray eps = recent.getJSONArray("episodes");
|
||||
Episode e0 = Episode.fromApi(eps.getJSONObject(0), null, null, null);
|
||||
check(eps.length() == 20 && e0 != null, "recent episodes parsed");
|
||||
check(e0.show.equals("Mini Show") && e0.author.equals("Ana Álvarez") && e0.itemId.equals("pod0"),
|
||||
"show name/author taken from nested podcast");
|
||||
check(!eps.getJSONObject(0).getJSONObject("podcast").has("episodes"),
|
||||
"nested podcast episode lists dropped while reading (memory)");
|
||||
check(e0.ino.equals("ino100") && e0.ext.equals(".mp3") && e0.duration > 1900 && e0.size > 0,
|
||||
"audio file details parsed");
|
||||
check(Util.date(e0.published).length() > 0 && Util.duration(e0.duration).equals("32 min"),
|
||||
"date and duration formatted (" + Util.date(e0.published) + ", " + Util.duration(e0.duration) + ")");
|
||||
|
||||
App.loadProgress(c);
|
||||
double[] pr7 = App.progress("ep7"), pr8 = App.progress("ep8");
|
||||
check(pr7 != null && pr7[0] == 600.0 && pr7[2] == 0, "in-progress episode position loaded");
|
||||
check(pr8 != null && pr8[2] == 1, "finished episode flagged");
|
||||
|
||||
JSONArray ip = Api.itemsInProgress(c).getJSONArray("libraryItems");
|
||||
JSONObject it0 = ip.getJSONObject(0);
|
||||
Episode cont = Episode.fromApi(it0.optJSONObject("recentEpisode"), it0.getString("id"), "Mini Show", "Ana");
|
||||
check(cont != null && cont.id.equals("ep7"), "continue-listening episode parsed");
|
||||
check(Episode.fromApi(ip.getJSONObject(1).optJSONObject("recentEpisode"), "book9", "", "") == null,
|
||||
"books without an episode skipped");
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
JSONObject item = Api.item(c, "pod1");
|
||||
JSONArray all = item.getJSONObject("media").getJSONArray("episodes");
|
||||
int maxDesc = 0;
|
||||
for (int i = 0; i < all.length(); i++) maxDesc = Math.max(maxDesc, all.getJSONObject(i).getString("description").length());
|
||||
check(all.length() == 300, "300-episode podcast loaded in " + (System.currentTimeMillis() - t0) + " ms");
|
||||
check(maxDesc <= 801, "show notes capped at 800 chars (max " + maxDesc + ")");
|
||||
check(!item.has("libraryFiles") && !all.getJSONObject(0).has("chapters"), "file lists and chapters dropped");
|
||||
Episode e7 = Episode.fromApi(all.getJSONObject(7), "pod1", "Mini Show", "Ana");
|
||||
check(Util.plainText(e7.desc).startsWith("Show notes café"), "show notes become plain text");
|
||||
|
||||
System.out.println("Playback session");
|
||||
JSONObject sess = Api.startPlayback(c, e7);
|
||||
check(Util.str(sess, "id").equals("sess1") && sess.getDouble("currentTime") == 600.0, "session opened at saved position");
|
||||
String cu = sess.getJSONArray("audioTracks").getJSONObject(0).getString("contentUrl");
|
||||
String url = Api.withToken(c, Api.absolute(c, cu));
|
||||
check(url.startsWith(base + "/api/items/pod1/file/ino7?token=") && url.endsWith(Api.enc(p.token())),
|
||||
"stream URL carries the token");
|
||||
HttpURLConnection sc = (HttpURLConnection) new URL(url).openConnection();
|
||||
check(sc.getResponseCode() != 401, "server accepts the tokenised stream URL");
|
||||
sc.disconnect();
|
||||
Api.syncSession(c, "sess1", 615.5, 15.2, 1807.5);
|
||||
Api.closeSession(c, "sess1", 620, 4.5, 1807.5);
|
||||
Api.setProgress(c, "pod1", "ep7", 1807.5, 1807.5, Boolean.TRUE);
|
||||
JSONArray log = (JSONArray) server(base, "/__log");
|
||||
JSONObject sync = null, close = null, patch = null, play = null;
|
||||
for (int i = 0; i < log.length(); i++) {
|
||||
JSONObject r = log.getJSONObject(i);
|
||||
String path = r.getString("p");
|
||||
if (path.equals("/api/session/sess1/sync")) sync = r;
|
||||
if (path.equals("/api/session/sess1/close")) close = r;
|
||||
if (path.equals("/api/me/progress/pod1/ep7")) patch = r;
|
||||
if (path.startsWith("/api/items/pod1/play/")) play = r;
|
||||
}
|
||||
check(play != null && play.getJSONObject("b").getBoolean("forceDirectPlay")
|
||||
&& play.getJSONObject("b").getJSONObject("deviceInfo").getString("clientName").equals("MiniABS"),
|
||||
"play request asks for direct play and names the client");
|
||||
check(sync != null && sync.getJSONObject("b").getDouble("currentTime") == 615.5
|
||||
&& sync.getJSONObject("b").getDouble("timeListened") == 15.2, "progress sync sent");
|
||||
check(close != null, "session closed");
|
||||
check(patch != null && patch.getString("m").equals("PATCH")
|
||||
&& patch.getJSONObject("b").getBoolean("isFinished"), "mark-as-played sent as PATCH");
|
||||
|
||||
System.out.println("Offline catch-up");
|
||||
Episode e9 = Episode.fromApi(all.getJSONObject(9), "pod1", "Mini Show", "Ana");
|
||||
App.saveLocal(c, e9, 321.0, 1809.5, true);
|
||||
check(App.localPending(c, "ep9") && App.localPosition(c, "ep9") == 321.0, "position saved on the phone");
|
||||
App.loadProgress(c);
|
||||
check(App.progress("ep9")[0] == 321.0, "unsent local position wins over server's");
|
||||
int sent = App.flushPending(c);
|
||||
check(sent == 1 && !App.localPending(c, "ep9"), "pending position sent when back online");
|
||||
|
||||
if (!legacy) {
|
||||
System.out.println("Token refresh");
|
||||
String before = p.token();
|
||||
server(base, "/__expire");
|
||||
JSONArray again = Api.libraries(c);
|
||||
check(again.length() == 2 && !p.token().equals(before), "expired token refreshed transparently after 401");
|
||||
check(p.refreshToken().equals("refresh-" + (p.token().contains("\"n\"") ? "" : "")) || p.refreshToken().startsWith("refresh-"),
|
||||
"rotated refresh token stored");
|
||||
server(base, "/__expire");
|
||||
int r0 = ((JSONObject) server(base, "/__refreshes")).getInt("n");
|
||||
final Context fc = c;
|
||||
final Exception[] errs = new Exception[3];
|
||||
Thread[] ts = new Thread[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int k = i;
|
||||
ts[i] = new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
Api.libraries(fc);
|
||||
} catch (Exception ex) {
|
||||
errs[k] = ex;
|
||||
}
|
||||
}
|
||||
};
|
||||
ts[i].start();
|
||||
}
|
||||
for (Thread t : ts) t.join();
|
||||
int r1 = ((JSONObject) server(base, "/__refreshes")).getInt("n");
|
||||
check(errs[0] == null && errs[1] == null && errs[2] == null && r1 - r0 == 1,
|
||||
"3 requests failing at once trigger exactly one refresh (" + (r1 - r0) + ")");
|
||||
// Simulate the token nearing expiry on the phone's clock: refreshed before the request.
|
||||
p.sp.edit().putLong("token_obtained", System.currentTimeMillis() - 3550 * 1000L).commit();
|
||||
String old = p.token();
|
||||
Api.libraries(c);
|
||||
check(!p.token().equals(old), "token refreshed early when about to expire");
|
||||
server(base, "/__revoke");
|
||||
try {
|
||||
Api.libraries(c);
|
||||
check(false, "revoked sign-in detected");
|
||||
} catch (Api.AuthException ex) {
|
||||
check(true, "revoked sign-in sends user back to login: \"" + ex.getMessage() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Sub-path servers");
|
||||
p.setServer("http://nas.local:8080/abs", false);
|
||||
check(Api.absolute(c, "/abs/api/items/x/file/1").equals("http://nas.local:8080/abs/api/items/x/file/1"),
|
||||
"content URL under a sub-path not doubled");
|
||||
check(Api.absolute(c, "/api/items/x/file/1").equals("http://nas.local:8080/abs/api/items/x/file/1"),
|
||||
"content URL without the sub-path prefixed");
|
||||
|
||||
System.out.println();
|
||||
System.out.println(pass + " passed, " + fail + " failed");
|
||||
System.exit(fail == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
/** Same rules as LoginActivity.normalize (the Activity itself can't load off-device). */
|
||||
static String LoginActivity_normalize(String s) {
|
||||
s = s.trim();
|
||||
while (s.endsWith("/")) s = s.substring(0, s.length() - 1);
|
||||
if (s.endsWith("/login")) s = s.substring(0, s.length() - 6);
|
||||
if (s.length() > 0 && !s.startsWith("http://") && !s.startsWith("https://")) s = "http://" + s;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
"""A small mock of the Audiobookshelf 2.x API, shaped like the real server's replies.
|
||||
|
||||
Usage: python3 mock_abs.py PORT [legacy]
|
||||
legacy: /login returns only the old long-lived user.token (pre-2.26 servers).
|
||||
Test-only endpoints: /__expire (invalidate the access token), /__revoke (invalidate the
|
||||
refresh token too), /__log (requests seen so far).
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
PORT = int(sys.argv[1])
|
||||
LEGACY = len(sys.argv) > 2 and sys.argv[2] == "legacy"
|
||||
lock = threading.Lock()
|
||||
state = {"access": None, "refresh": None, "n": 0, "log": [], "progress": {}, "refreshes": 0}
|
||||
|
||||
|
||||
def b64(d):
|
||||
return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=").decode()
|
||||
|
||||
|
||||
def new_tokens():
|
||||
state["n"] += 1
|
||||
now = int(time.time())
|
||||
state["access"] = b64({"alg": "HS256"}) + "." + b64({"userId": "u1", "iat": now, "exp": now + 3600, "n": state["n"]}) + ".sig"
|
||||
state["refresh"] = "refresh-%d" % state["n"]
|
||||
|
||||
|
||||
def episode(i, item="pod1", long_desc=False):
|
||||
desc = ("<p>Show notes café \U0001F3A7 " + "x" * (5000 if long_desc else 50) + "</p>")
|
||||
return {
|
||||
"libraryItemId": item, "podcastId": "p-" + item, "id": "ep%d" % i, "index": i,
|
||||
"title": "Episode %d – ¿Qué tal?" % i, "subtitle": "", "description": desc,
|
||||
"publishedAt": 1700000000000 + i * 86400000, "pubDate": "x",
|
||||
"chapters": [{"id": c, "start": c * 60, "end": c * 60 + 60, "title": "ch"} for c in range(30)],
|
||||
"enclosure": {"url": "http://feed/ep%d.mp3" % i, "type": "audio/mpeg", "length": "123"},
|
||||
"audioFile": {"index": 1, "ino": "ino%d" % i, "duration": 1800.5 + i, "mimeType": "audio/mpeg",
|
||||
"metadata": {"filename": "ep%d.mp3" % i, "ext": ".mp3", "size": 30000000 + i},
|
||||
"metaTags": {"tagAlbum": "x"}},
|
||||
}
|
||||
|
||||
|
||||
def podcast_media(n_eps, item="pod1"):
|
||||
return {"id": "p-" + item, "metadata": {"title": "Mini Show", "author": "Ana Álvarez"},
|
||||
"episodes": [episode(i, item, long_desc=True) for i in range(n_eps)]}
|
||||
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def send(self, code, body=None, ctype="application/json"):
|
||||
data = b""
|
||||
if body is not None:
|
||||
data = (json.dumps(body) if ctype == "application/json" else body).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", ctype + "; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def body(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(n) if n else b""
|
||||
return json.loads(raw) if raw else None
|
||||
|
||||
def authed(self):
|
||||
auth = self.headers.get("Authorization", "")
|
||||
tok = auth[7:] if auth.startswith("Bearer ") else None
|
||||
if tok is None and "token=" in self.path:
|
||||
tok = self.path.split("token=")[1].split("&")[0]
|
||||
return tok is not None and tok == state["access"]
|
||||
|
||||
def handle_any(self, method):
|
||||
path = self.path.split("?")[0]
|
||||
b = self.body() if method in ("POST", "PATCH") else None
|
||||
with lock:
|
||||
state["log"].append({"m": method, "p": self.path, "b": b,
|
||||
"rt": self.headers.get("x-return-tokens"), "xr": self.headers.get("x-refresh-token")})
|
||||
if path == "/__log":
|
||||
return self.send(200, state["log"])
|
||||
if path == "/__refreshes":
|
||||
return self.send(200, {"n": state["refreshes"]})
|
||||
if path == "/__expire":
|
||||
state["access"] = "expired"
|
||||
return self.send(200, {})
|
||||
if path == "/__revoke":
|
||||
state["access"] = "expired"
|
||||
state["refresh"] = "revoked"
|
||||
return self.send(200, {})
|
||||
if path == "/status":
|
||||
return self.send(200, {"app": "audiobookshelf", "serverVersion": "2.30.0", "isInit": True,
|
||||
"authMethods": ["local"]})
|
||||
if path == "/login" and method == "POST":
|
||||
if not b or b.get("username") != "demo" or b.get("password") != "pw":
|
||||
return self.send(401, "Unauthorized", "text/plain")
|
||||
new_tokens()
|
||||
user = {"id": "u1", "username": "demo", "token": state["access"] if LEGACY else "legacy-tok"}
|
||||
if LEGACY:
|
||||
user["token"] = state["access"]
|
||||
elif self.headers.get("x-return-tokens") == "true":
|
||||
user["accessToken"] = state["access"]
|
||||
user["refreshToken"] = state["refresh"]
|
||||
return self.send(200, {"user": user, "userDefaultLibraryId": "lib-pod",
|
||||
"serverSettings": {}})
|
||||
if path == "/auth/refresh" and method == "POST":
|
||||
if self.headers.get("x-refresh-token") != state["refresh"]:
|
||||
return self.send(401, "Unauthorized", "text/plain")
|
||||
state["refreshes"] += 1
|
||||
new_tokens()
|
||||
return self.send(200, {"user": {"id": "u1", "accessToken": state["access"],
|
||||
"refreshToken": state["refresh"]}})
|
||||
if not self.authed():
|
||||
# Real ABS answers 401 without a WWW-Authenticate header
|
||||
return self.send(401, "Unauthorized", "text/plain")
|
||||
|
||||
if path == "/api/libraries":
|
||||
return self.send(200, {"libraries": [
|
||||
{"id": "lib-book", "name": "Books", "mediaType": "book"},
|
||||
{"id": "lib-pod", "name": "Podcasts", "mediaType": "podcast"}]})
|
||||
if path == "/api/libraries/lib-pod/recent-episodes":
|
||||
eps = []
|
||||
for i in range(20):
|
||||
e = episode(100 + i, "pod%d" % (i % 3))
|
||||
e["podcast"] = podcast_media(40, "pod%d" % (i % 3)) # the real server nests the whole podcast
|
||||
e["podcast"]["libraryItem"] = {"id": "pod%d" % (i % 3), "libraryId": "lib-pod"}
|
||||
eps.append(e)
|
||||
return self.send(200, {"episodes": eps, "limit": 20, "page": 0})
|
||||
if path == "/api/me/items-in-progress":
|
||||
return self.send(200, {"libraryItems": [
|
||||
{"id": "pod1", "libraryId": "lib-pod", "mediaType": "podcast",
|
||||
"media": {"metadata": {"title": "Mini Show", "author": "Ana"}, "numEpisodes": 300},
|
||||
"recentEpisode": episode(7), "progressLastUpdate": 1},
|
||||
{"id": "book9", "libraryId": "lib-book", "mediaType": "book", "media": {"metadata": {"title": "B"}}}]})
|
||||
if path == "/api/me":
|
||||
mp = [{"libraryItemId": "pod1", "episodeId": "ep7", "currentTime": 600.0, "duration": 1807.5,
|
||||
"progress": 0.33, "isFinished": False},
|
||||
{"libraryItemId": "pod1", "episodeId": "ep8", "currentTime": 1808.5, "duration": 1808.5,
|
||||
"progress": 1, "isFinished": True},
|
||||
{"libraryItemId": "book9", "episodeId": None, "currentTime": 5, "isFinished": False}]
|
||||
return self.send(200, {"id": "u1", "username": "demo", "mediaProgress": mp, "bookmarks": []})
|
||||
if path == "/api/libraries/lib-pod/items":
|
||||
res = [{"id": "pod%d" % i, "libraryId": "lib-pod",
|
||||
"media": {"metadata": {"title": "Show %d" % i, "author": "A"}, "numEpisodes": i}}
|
||||
for i in range(3)]
|
||||
return self.send(200, {"results": res, "total": 3, "limit": 100, "page": 0})
|
||||
if path == "/api/items/pod1":
|
||||
item = {"id": "pod1", "libraryId": "lib-pod", "media": podcast_media(300),
|
||||
"libraryFiles": [{"ino": "x%d" % i, "metadata": {"path": "/p" * 20}} for i in range(300)]}
|
||||
return self.send(200, item)
|
||||
if path.startswith("/api/items/pod1/play/") and method == "POST":
|
||||
return self.send(200, {"id": "sess1", "libraryItemId": "pod1", "episodeId": path.split("/")[-1],
|
||||
"currentTime": 600.0, "duration": 1807.5, "playMethod": 0,
|
||||
"audioTracks": [{"index": 1, "contentUrl": "/api/items/pod1/file/ino7",
|
||||
"mimeType": "audio/mpeg", "duration": 1807.5}]})
|
||||
if path.startswith("/api/session/") and method == "POST":
|
||||
return self.send(200, "OK", "text/plain")
|
||||
if path.startswith("/api/me/progress/") and method == "PATCH":
|
||||
state["progress"][path] = b
|
||||
return self.send(200, "OK", "text/plain")
|
||||
return self.send(404, "Not Found", "text/plain")
|
||||
|
||||
def do_GET(self):
|
||||
self.handle_any("GET")
|
||||
|
||||
def do_POST(self):
|
||||
self.handle_any("POST")
|
||||
|
||||
def do_PATCH(self):
|
||||
self.handle_any("PATCH")
|
||||
|
||||
|
||||
ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Runs MiniABS's API/JSON/progress code on the desktop JVM against mock_abs.py.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=build/test
|
||||
JSON_JAR=/usr/share/java/com.android.json.jar
|
||||
rm -rf "$OUT" && mkdir -p "$OUT"
|
||||
S=src/com/miniabs
|
||||
javac -nowarn -encoding UTF-8 -cp "$JSON_JAR" -d "$OUT" $(find test/shim -name '*.java') \
|
||||
$S/Json.java $S/Prefs.java $S/Episode.java $S/Util.java $S/Api.java $S/App.java test/com/miniabs/ApiTest.java
|
||||
|
||||
for mode in current legacy; do
|
||||
PORT=$((18000 + $(od -An -N2 -tu2 /dev/urandom | tr -d ' ') % 1000))
|
||||
python3 test/mock_abs.py $PORT $mode &
|
||||
PID=$!
|
||||
sleep 1
|
||||
echo "=== $mode server"
|
||||
java -cp "$OUT:$JSON_JAR" com.miniabs.ApiTest $PORT $mode || { kill $PID; exit 1; }
|
||||
kill $PID
|
||||
done
|
||||
@@ -0,0 +1,15 @@
|
||||
package android.content;
|
||||
import java.util.HashMap;
|
||||
public class Context {
|
||||
public static final int MODE_PRIVATE = 0;
|
||||
public static final String CONNECTIVITY_SERVICE = "connectivity";
|
||||
private final HashMap<String, SharedPreferences> prefs = new HashMap<String, SharedPreferences>();
|
||||
public static boolean online = true;
|
||||
public Context getApplicationContext() { return this; }
|
||||
public synchronized SharedPreferences getSharedPreferences(String name, int mode) {
|
||||
SharedPreferences p = prefs.get(name);
|
||||
if (p == null) { p = new SharedPreferences(); prefs.put(name, p); }
|
||||
return p;
|
||||
}
|
||||
public Object getSystemService(String name) { return new android.net.ConnectivityManager(); }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package android.content;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
public class SharedPreferences {
|
||||
final HashMap<String, Object> m = new HashMap<String, Object>();
|
||||
public synchronized String getString(String k, String d) { Object v = m.get(k); return v == null ? d : (String) v; }
|
||||
public synchronized boolean getBoolean(String k, boolean d) { Object v = m.get(k); return v == null ? d : (Boolean) v; }
|
||||
public synchronized long getLong(String k, long d) { Object v = m.get(k); return v == null ? d : (Long) v; }
|
||||
public synchronized int getInt(String k, int d) { Object v = m.get(k); return v == null ? d : (Integer) v; }
|
||||
public synchronized Map<String, ?> getAll() { return new HashMap<String, Object>(m); }
|
||||
public Editor edit() { return new Editor(); }
|
||||
public class Editor {
|
||||
final HashMap<String, Object> put = new HashMap<String, Object>(); final java.util.HashSet<String> rm = new java.util.HashSet<String>();
|
||||
public Editor putString(String k, String v) { put.put(k, v); return this; }
|
||||
public Editor putBoolean(String k, boolean v) { put.put(k, v); return this; }
|
||||
public Editor putLong(String k, long v) { put.put(k, v); return this; }
|
||||
public Editor putInt(String k, int v) { put.put(k, v); return this; }
|
||||
public Editor remove(String k) { rm.add(k); return this; }
|
||||
public boolean commit() { synchronized (SharedPreferences.this) { for (String k : rm) m.remove(k); m.putAll(put); } return true; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package android.net;
|
||||
public class ConnectivityManager {
|
||||
public static final int TYPE_WIFI = 1;
|
||||
public NetworkInfo getActiveNetworkInfo() { return android.content.Context.online ? new NetworkInfo() : null; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package android.net;
|
||||
public class NetworkInfo { public boolean isConnected() { return true; } public int getType() { return 1; } }
|
||||
@@ -0,0 +1,5 @@
|
||||
package android.os;
|
||||
public class Build {
|
||||
public static final String MANUFACTURER = "samsung", MODEL = "GT-S5570";
|
||||
public static class VERSION { public static final String RELEASE = "2.2.1"; public static final int SDK_INT = 8; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package android.preference;
|
||||
import android.content.Context; import android.content.SharedPreferences;
|
||||
public class PreferenceManager {
|
||||
public static SharedPreferences getDefaultSharedPreferences(Context c) { return c.getSharedPreferences("default", 0); }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package android.text;
|
||||
public class Html { public static CharSequence fromHtml(String s) { return s.replaceAll("<[^>]+>", ""); } }
|
||||
@@ -0,0 +1,5 @@
|
||||
package android.util;
|
||||
public class Base64 {
|
||||
public static final int URL_SAFE = 8;
|
||||
public static byte[] decode(String s, int flags) { return java.util.Base64.getUrlDecoder().decode(s); }
|
||||
}
|
||||
Reference in new issue
Block a user