From 58d21b6d686c652d4ecb8e3154d5c1567e7fca4a Mon Sep 17 00:00:00 2001 From: xocko Date: Wed, 14 Oct 2026 20:56:27 +0200 Subject: [PATCH] Minamp 1.3 - Fix NoSuchMethodError on Android 2.x: Dalvik before 4.0 can't link Object methods (toString/equals/...) called through an interface. Patch the bundled Bouncy Castle (tools/patch_bc_dalvik.py) and our own CertVerifier, and make build.sh fail if tools/scan_dalvik.py finds any such call - Treat a manually entered private address (10.x, 172.16-31.x, 192.168.x, .local) as home, so LAN servers that allow access without sign-in work - Send Plex client headers on media requests (files and transcodes) - Explain a 503 on a file as the server being unable to open it (drive or folder not available) - Connection log: home/away, token presence, Plex's file check and a direct file request in the streaming test Co-Authored-By: Claude Opus 5.5 --- AndroidManifest.xml | 4 +-- build.sh | 6 ++++ src/com/minamp/BaseActivity.java | 19 ++++++++++-- src/com/minamp/ImageLoader.java | 2 +- src/com/minamp/PlayerService.java | 5 +-- src/com/minamp/net/CertVerifier.java | 3 +- src/com/minamp/net/StreamProxy.java | 13 +++++++- src/com/minamp/plex/Library.java | 20 +++++++++++- src/com/minamp/plex/Server.java | 31 ++++++++++++++++++- test/PlexTest.java | 26 ++++++++++++++++ test/mock_plex.py | 13 ++++++-- tools/patch_bc_dalvik.py | 42 +++++++++++++++++++++++++ tools/scan_dalvik.py | 46 ++++++++++++++++++++++++++++ tools/setup_toolchain.sh | 1 + 14 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 tools/patch_bc_dalvik.py create mode 100644 tools/scan_dalvik.py diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 09ada31..c51a895 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ diff --git a/build.sh b/build.sh index bec1f4f..65b7376 100755 --- a/build.sh +++ b/build.sh @@ -22,6 +22,12 @@ javac -nowarn -Xlint:-options -source 8 -target 8 -encoding UTF-8 -g:source,line -d $OUT/classes @$OUT/sources.txt 2>&1 | grep -v -E '^Picked up|^Note:' || true test -f $OUT/classes/com/minamp/HomeActivity.class +echo "== Dalvik 2.x link check" +python3 tools/scan_dalvik.py $OUT/classes $ANDROID_JAR > $OUT/dalvik-scan.txt +if ! grep -q "^total patterns: 0$" $OUT/dalvik-scan.txt; then + cat $OUT/dalvik-scan.txt; echo "Calls Android 2.x can't link (see above): fix before shipping"; exit 1 +fi + echo "== dex" java -Xmx2g -jar $T/dx.jar --dex --output=$OUT/classes.dex $OUT/classes 2>&1 | grep -v '^Picked up' || true diff --git a/src/com/minamp/BaseActivity.java b/src/com/minamp/BaseActivity.java index 5e8ccf1..206d85e 100644 --- a/src/com/minamp/BaseActivity.java +++ b/src/com/minamp/BaseActivity.java @@ -499,7 +499,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis } lib.ensureMedia(t); com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via " - + (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind))); + + (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind)) + + (lib.server.isLocal() ? " (home)" : " (away)")); + String tok = lib.server.token; + com.minamp.net.DiagLog.log("token: " + (tok == null || tok.length() == 0 ? "none (relying on the server allowing this network without sign-in)" : "set, " + tok.length() + " chars")); + com.minamp.net.DiagLog.log("server file check: " + lib.checkPart(t)); + com.minamp.net.Http.Response fr = null; + try { + java.util.Map h = lib.server.mediaHeaders(); + h.put("Range", "bytes=0-15"); + fr = com.minamp.net.Http.get(lib.directUrl(t), h, 20000); + com.minamp.net.DiagLog.log("original file request: " + fr.code + " " + (fr.code >= 400 ? com.minamp.plex.Plex.snippet(fr.readString()) : fr.header("content-type"))); + } catch (Exception e) { + com.minamp.net.DiagLog.log("original file request failed: " + com.minamp.net.Threads.describe(e)); + } finally { + com.minamp.net.Http.abort(fr); + } int kbps = app.currentMaxKbps(); if (kbps <= 0) kbps = 320; for (int v = 0; v < Library.VARIANTS; v++) { @@ -508,7 +523,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis String url = lib.transcodeUrl(t, kbps, 0, session, v); com.minamp.net.Http.Response r = null; try { - r = com.minamp.net.Http.get(url, null, 30000); + r = com.minamp.net.Http.get(url, lib.server.mediaHeaders(), 30000); if (r.code < 400) { byte[] buf = new byte[4096]; int n = r.body.read(buf); diff --git a/src/com/minamp/ImageLoader.java b/src/com/minamp/ImageLoader.java index 8d4d234..155ab62 100644 --- a/src/com/minamp/ImageLoader.java +++ b/src/com/minamp/ImageLoader.java @@ -161,7 +161,7 @@ public final class ImageLoader { if (data == null) { if (app.server == null) return null; String url = app.server.imageUrl(j.thumb, j.size); - Http.Response r = Http.get(url, null, 20000); + Http.Response r = Http.get(url, app.server.mediaHeaders(), 20000); if (r.code != 200) { r.close(); return null; } data = r.readAll(); try { diff --git a/src/com/minamp/PlayerService.java b/src/com/minamp/PlayerService.java index f61d8f2..dc04512 100644 --- a/src/com/minamp/PlayerService.java +++ b/src/com/minamp/PlayerService.java @@ -320,7 +320,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList Http.Response r = null; String why = null; for (int i = 0; i < urls.size(); i++) { - r = Http.get(urls.get(i), Plex.headers(null), 30000); + r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000); if (r.code == 200) { if (order != null) { final int idx = i; @@ -397,7 +397,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList refusalCode = 0; acceptedVariant = -1; // transcodes are fetched once and buffered, so player reconnects can't restart them - mp.setDataSource(proxy.register(routeId, urls, ext, transcoding)); + mp.setDataSource(proxy.register(routeId, urls, ext, transcoding, + App.get().server != null ? App.get().server.mediaHeaders() : null)); } setState(BUFFERING, "Buffering…"); mp.prepareAsync(); diff --git a/src/com/minamp/net/CertVerifier.java b/src/com/minamp/net/CertVerifier.java index 7fcecae..9f5c9d3 100644 --- a/src/com/minamp/net/CertVerifier.java +++ b/src/com/minamp/net/CertVerifier.java @@ -160,7 +160,8 @@ public final class CertVerifier { GeneralName n = arr[i]; if (n.getTagNo() == GeneralName.dNSName) { sawDns = true; - if (!ip && dnsMatch(n.getName().toString(), host)) return true; + // String.valueOf, not getName().toString(): Dalvik < 4.0 can't call Object methods via an interface + if (!ip && dnsMatch(String.valueOf(n.getName()), host)) return true; } else if (n.getTagNo() == GeneralName.iPAddress && ip) { byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets(); if (ipString(a).equals(host)) return true; diff --git a/src/com/minamp/net/StreamProxy.java b/src/com/minamp/net/StreamProxy.java index e2e0f2d..43f1328 100644 --- a/src/com/minamp/net/StreamProxy.java +++ b/src/com/minamp/net/StreamProxy.java @@ -50,6 +50,7 @@ public final class StreamProxy implements Runnable { final String id; final List urls; final boolean buffered; + Map headers; // sent upstream with every request int chosen = -1; // index that worked final List live = new ArrayList(); // buffered state (guarded by this) @@ -95,8 +96,14 @@ public final class StreamProxy implements Runnable { /** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */ public String register(String id, List remoteUrls, String ext, boolean buffered) { + return register(id, remoteUrls, ext, buffered, null); + } + + public String register(String id, List remoteUrls, String ext, boolean buffered, Map headers) { + Route r = new Route(id, new ArrayList(remoteUrls), buffered); + r.headers = headers; synchronized (routes) { - routes.put(id, new Route(id, new ArrayList(remoteUrls), buffered)); + routes.put(id, r); } return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext; } @@ -198,6 +205,7 @@ public final class StreamProxy implements Runnable { /** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */ private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException { Map h = new LinkedHashMap(); + if (route.headers != null) h.putAll(route.headers); h.put("Accept", "*/*"); if (range != null) h.put("Range", range); String refusal = null; @@ -216,6 +224,9 @@ public final class StreamProxy implements Runnable { up.close(); code = up.code; refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : ""); + if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) { + refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?"; + } DiagLog.log("stream variant " + i + " refused: " + refusal); } throw new RefusedException(code, refusal); diff --git a/src/com/minamp/plex/Library.java b/src/com/minamp/plex/Library.java index 8ff93fa..42f9d73 100644 --- a/src/com/minamp/plex/Library.java +++ b/src/com/minamp/plex/Library.java @@ -251,12 +251,30 @@ public final class Library { return server.url(base + "?" + q); } + /** Whether the server can actually read a track's file (Plex's checkFiles), for diagnostics. */ + public String checkPart(Item t) { + try { + Json.Obj mc = server.get("/library/metadata/" + t.ratingKey + "?checkFiles=1"); + Json.Arr md = mc == null ? null : mc.optArr("Metadata"); + Json.Obj m = md == null ? null : md.optObj(0); + Json.Arr media = m == null ? null : m.optArr("Media"); + Json.Obj me = media == null ? null : media.optObj(0); + Json.Arr parts = me == null ? null : me.optArr("Part"); + Json.Obj p = parts == null ? null : parts.optObj(0); + if (p == null) return "no file information returned"; + return "exists=" + p.optString("exists", "?") + " accessible=" + p.optString("accessible", "?") + + " file=" + p.optString("file", "?"); + } catch (Exception e) { + return com.minamp.net.Threads.describe(e); + } + } + /** Ask the server what it would do with a transcode request (for the diagnostics screen). */ public String decision(Item t, int kbps, int variant) { try { String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant); url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?"); - com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, Plex.headers(null), 20000); + com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, server.mediaHeaders(), 20000); String body = r.readString(); if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : ""); Json.Obj mc = Json.parseObj(body).optObj("MediaContainer"); diff --git a/src/com/minamp/plex/Server.java b/src/com/minamp/plex/Server.java index 17788fa..9ceca66 100644 --- a/src/com/minamp/plex/Server.java +++ b/src/com/minamp/plex/Server.java @@ -73,7 +73,28 @@ public final class Server { public boolean isLocal() { Conn c = active; - return c != null && (c.kind == LOCAL || c.kind == LOCAL_HTTP); + if (c == null) return false; + if (c.kind == LOCAL || c.kind == LOCAL_HTTP) return true; + return c.kind == MANUAL && isPrivateAddress(c.uri); + } + + /** A typed-in address on the home network (10.x, 172.16-31.x, 192.168.x, .local) counts as home. */ + static boolean isPrivateAddress(String uri) { + String h = uri; + int s = h.indexOf("://"); + if (s >= 0) h = h.substring(s + 3); + int end = h.length(); + for (int i = 0; i < h.length(); i++) { char ch = h.charAt(i); if (ch == ':' || ch == '/') { end = i; break; } } + h = h.substring(0, end).toLowerCase(); + if (h.endsWith(".local") || h.equals("localhost")) return true; + if (h.startsWith("10.") || h.startsWith("192.168.") || h.startsWith("127.")) return true; + if (h.startsWith("172.")) { + try { + int second = Integer.parseInt(h.substring(4, h.indexOf('.', 4))); + return second >= 16 && second <= 31; + } catch (Exception e) { return false; } + } + return false; } public boolean isRelay() { @@ -202,9 +223,17 @@ public final class Server { /** Absolute URL with the token in the query string (for the stream proxy and images). */ public String url(String path) throws IOException { + if (token == null || token.length() == 0) return base() + path; // never send an empty token return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token); } + /** Headers for media requests (files, transcodes): Plex wants to know which client is asking. */ + public java.util.Map mediaHeaders() { + java.util.Map h = Plex.headers(token); + h.put("Accept", "*/*"); + return h; + } + public String imageUrl(String thumb, int size) throws IOException { if (thumb == null || thumb.length() == 0) return null; return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb)); diff --git a/test/PlexTest.java b/test/PlexTest.java index 478483d..cbdc1d8 100644 --- a/test/PlexTest.java +++ b/test/PlexTest.java @@ -196,6 +196,32 @@ public class PlexTest { Server.Conn c3 = s3.pick(); check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri); + // --- manual LAN address without a token (server allows this network without sign-in) + Server lan = new Server(); + lan.name = "manual"; lan.token = ""; + lan.add(Server.normalizeManual("127.0.0.1"), Server.MANUAL); + lan.pick(); + check(lan.isLocal(), "manual private address counts as home (original quality)"); + check(lan.url("/library/parts/501/1/file.mp3").indexOf("X-Plex-Token") < 0, "no empty X-Plex-Token on media URLs"); + Library lanLib = new Library(lan, "1"); + Item lanTrack = lanLib.tracksOf(album).get(0); + String lid = proxy.newId(); + String lurl = proxy.register(lid, java.util.Collections.singletonList(lanLib.directUrl(lanTrack)), "mp3", false, lan.mediaHeaders()); + hc = (HttpURLConnection) new URL(lurl).openConnection(Proxy.NO_PROXY); + byte[] lb = read(hc.getInputStream()); + check(hc.getResponseCode() == 200 && lb.length == 300000, "token-less LAN: original MP3 streams (" + hc.getResponseCode() + ")"); + Item flac = lanLib.tracksOf(album).get(1); + String lid2 = proxy.newId(); + String lurl2 = proxy.register(lid2, lanLib.transcodeUrls(flac, 192, 0, "lan1"), "mp3", true, lan.mediaHeaders()); + hc = (HttpURLConnection) new URL(lurl2).openConnection(Proxy.NO_PROXY); + byte[] lt = read(hc.getInputStream()); + check(hc.getResponseCode() == 200 && new String(lt, "ISO-8859-1").startsWith("TRANSCODE"), "token-less LAN: FLAC converts to MP3"); + // and the old behaviour reproduces the phone's log exactly + Http.Response o1 = Http.get(lanLib.directUrl(lanTrack) + "?X-Plex-Token=", null, 10000); o1.readAll(); + Http.Response o2 = Http.get(lanLib.transcodeUrls(flac, 192, 0, "lan2").get(0) + "&X-Plex-Token=", null, 10000); o2.readAll(); + check(o1.code == 503 && o2.code == 400, "old empty-token URLs reproduce the log's 503 and 400"); + check(lanLib.checkPart(lanTrack).indexOf("exists=") >= 0 || lanLib.checkPart(lanTrack).length() > 0, "file check runs: " + lanLib.checkPart(lanTrack)); + // --- trust checks CertVerifier.setRootStore(roots(args[1])); // real Mozilla roots: test CA must be rejected Http.evictAll(); diff --git a/test/mock_plex.py b/test/mock_plex.py index 3ad398a..f1c2d20 100644 --- a/test/mock_plex.py +++ b/test/mock_plex.py @@ -107,16 +107,25 @@ class H(BaseHTTPRequestHandler): if n: self.rfile.read(n) u = urlparse(self.path) - q = parse_qs(u.query) + q = parse_qs(u.query, keep_blank_values=True) p = u.path host = self.headers.get("Host", "") tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0] + empty_token_param = "X-Plex-Token" in q and q["X-Plex-Token"][0] == "" and not self.headers.get("X-Plex-Token") log.append((method, host, self.path, tok, self.headers.get("Range"))) if host.startswith("plex.tv"): return self.tv(method, p, tok) if p == "/identity": return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}}) - if tok != TOKEN: + # Like a server with "allowed without auth" for its LAN: no token at all is fine from here, + # but media requests carrying an *empty* token are refused (503 files / 400 transcoder). + lan_no_auth = tok is None and self.server.server_address[1] == 32400 + if empty_token_param: + if p.startswith("/library/parts/"): + return self.send_json({"error": "Service Unavailable"}, 503) + if "/transcode/" in p: + return self.send_json({"error": "Bad Request"}, 400) + if tok != TOKEN and not lan_no_auth: return self.send_json({"error": "unauthorized"}, 401) if p == "/library/sections": return self.send_json({"MediaContainer": {"Directory": [ diff --git a/tools/patch_bc_dalvik.py b/tools/patch_bc_dalvik.py new file mode 100644 index 0000000..8213ee9 --- /dev/null +++ b/tools/patch_bc_dalvik.py @@ -0,0 +1,42 @@ +"""Rewrites Bouncy Castle calls that Android 2.x's Dalvik can't link. + +Calling an Object method (toString/equals/hashCode/getClass) through an *interface* type compiles +to invokeinterface, which Dalvik before 4.0 can't resolve: NoSuchMethodError at runtime. Casting +the receiver to Object makes javac emit invokevirtual instead. tools/scan_dalvik.py checks nothing +was missed. +""" +import sys + +FIXES = [ + ("asn1/ASN1InputStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"), + ("asn1/ConstructedBitStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"), + ("asn1/ConstructedOctetStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"), + ("asn1/x509/AlgorithmIdentifier.java", "return pa.equals(pb);", "return ((Object) pa).equals(pb);"), + ("asn1/x509/GeneralName.java", "buf.append(obj.toString());", "buf.append(((Object) obj).toString());"), + ("crypto/CryptoServicesPermission.java", "return actions.toString();", "return ((Object) actions).toString();"), + ("crypto/digests/SHA256Digest.java", "digest.getClass().getName()", "((Object) digest).getClass().getName()"), + ("crypto/engines/AESEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"), + ("crypto/engines/ARIAEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"), + ("crypto/engines/DESedeEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"), + ("crypto/engines/SM4Engine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"), + ("crypto/modes/CCMBlockCipher.java", "params.getClass().getName()", "((Object) params).getClass().getName()"), + ("math/ec/ECCurve.java", "getField().equals(other.getField())", "((Object) getField()).equals(other.getField())"), + ("math/ec/ECCurve.java", "return getField().hashCode()", "return ((Object) getField()).hashCode()"), + ("math/field/GenericPolynomialExtensionField.java", "return subfield.equals(other.subfield) && minimalPolynomial.equals(other.minimalPolynomial);", + "return ((Object) subfield).equals(other.subfield) && ((Object) minimalPolynomial).equals(other.minimalPolynomial);"), + ("math/field/GenericPolynomialExtensionField.java", "return subfield.hashCode()", "return ((Object) subfield).hashCode()"), + ("math/field/GenericPolynomialExtensionField.java", "rotateLeft(minimalPolynomial.hashCode(), 16)", "rotateLeft(((Object) minimalPolynomial).hashCode(), 16)"), + ("tls/crypto/impl/AbstractTlsCrypto.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"), + ("tls/crypto/impl/bc/BcTlsSecret.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"), +] + +root = sys.argv[1].rstrip("/") + "/com/minamp/bc/" +for rel, old, new in FIXES: + p = root + rel + s = open(p).read() + if new in s: + continue + if old not in s: + raise SystemExit("pattern not found in %s: %s" % (rel, old)) + open(p, "w").write(s.replace(old, new)) +print("patched %d call sites" % len(FIXES)) diff --git a/tools/scan_dalvik.py b/tools/scan_dalvik.py new file mode 100644 index 0000000..834ca4c --- /dev/null +++ b/tools/scan_dalvik.py @@ -0,0 +1,46 @@ +# Find invokeinterface calls to java.lang.Object methods (toString/equals/hashCode/getClass) through an +# interface that doesn't declare them itself: Dalvik before 4.0 can't resolve those (NoSuchMethodError). +import os, re, subprocess, sys, zipfile +classes = sys.argv[1]; android_jar = sys.argv[2] +OBJ = {"toString:()Ljava/lang/String;", "equals:(Ljava/lang/Object;)Z", "hashCode:()I", "getClass:()Ljava/lang/Class;"} +z = zipfile.ZipFile(android_jar) +declared = {} +def declares(iface, m): + key = (iface, m) + if key in declared: return declared[key] + path = os.path.join(classes, iface + ".class") + src = path if os.path.exists(path) else None + args = ["javap", "-p", "-s", "-cp", classes if src else android_jar, iface.replace("/", ".")] + out = subprocess.run(args, capture_output=True, text=True).stdout + name, desc = m.split(":") + found = False + lines = out.splitlines() + for i, l in enumerate(lines): + if re.search(r"\b" + name + r"\(", l) and i + 1 < len(lines) and desc in lines[i + 1]: + found = True + # superinterfaces declaring it also count + if not found: + mm = re.search(r"interface \S+ extends (.+?) \{", out) + if mm: + for sup in mm.group(1).split(","): + sup = sup.strip().split("<")[0].replace(".", "/") + if sup and declares(sup, m): found = True + declared[key] = found + return found +hits = {} +files = [os.path.join(dp, f) for dp, _, fs in os.walk(classes) for f in fs if f.endswith(".class")] +for i in range(0, len(files), 200): + out = subprocess.run(["javap", "-c", "-p"] + files[i:i+200], capture_output=True, text=True).stdout + cur = None + for line in out.splitlines(): + m = re.match(r"^(?:public |final |abstract |class |interface )*.*?(?:class|interface) (\S+)", line) + if m and not line.startswith(" "): cur = m.group(1) + mi = re.search(r"invokeinterface\s+#\d+,\s*\d+\s*// InterfaceMethod (\S+)\.(\w+):(\S+)", line) + if mi: + iface, name, desc = mi.group(1), mi.group(2), mi.group(3) + sig = name + ":" + desc + if sig in OBJ and not declares(iface, sig): + hits.setdefault((iface, sig), set()).add(cur) +for (iface, sig), users in sorted(hits.items()): + print(f"{iface}.{sig} <- {len(users)} class(es): " + ", ".join(sorted(users))[:300]) +print("total patterns:", len(hits)) diff --git a/tools/setup_toolchain.sh b/tools/setup_toolchain.sh index 9ebdd5a..61845c4 100755 --- a/tools/setup_toolchain.sh +++ b/tools/setup_toolchain.sh @@ -51,6 +51,7 @@ s = s.replace("return Long.divideUnsigned(x, y);", """if (y < 0) { return compar s = s.replace("return Long.parseUnsignedLong(s);", "return new java.math.BigInteger(s).longValue();") open(p, "w").write(s) PY + python3 "$PROJ/tools/patch_bc_dalvik.py" bcsrc fi echo "== signing key"