9 Commits
Author SHA1 Message Date
Mahdi Karzari 3b771faa21 Release FridaBox 4.1.0 for all Android ABIs 2026-07-28 12:52:59 +03:30
Mr.KarzariandGitHub bb608b4663 Merge pull request #12 from QM4RS/agent/gadget-manager-liquid-glass
Agent/gadget manager liquid glass
2026-07-27 16:56:35 +03:30
Mahdi Karzari ca9b32026d Add liquid bubble menu animations 2026-07-27 16:46:30 +03:30
Mahdi Karzari 4d0789ecd2 Add downloadable gadgets and liquid glass launcher 2026-07-27 16:03:29 +03:30
Mahdi Karzari 1d9ff51165 Update README branding with project logo 2026-07-27 14:13:25 +03:30
Mahdi Karzari 44cf537d89 Add floating log output menu and fix several issues 2026-07-27 14:04:49 +03:30
Mr.KarzariandGitHub e4b4f6e350 Fix home header logo and title layout (#11)
Use the current logo asset and a fixed-size custom toolbar layout so the home title and tagline stay aligned.
2026-07-23 22:40:22 +03:30
Mr.KarzariandGitHub 00cc4ebde3 build: consolidate compatible dependency updates (#10)
Update compatible CI actions, Kotlin, and Android test dependencies; repair sdkmanager discovery on current Ubuntu runners; and group future Dependabot updates.
2026-07-23 21:53:24 +03:30
Mahdi Karzari ab88654013 Icon updated 2026-07-23 21:20:26 +03:30
77 changed files with 3354 additions and 420 deletions
+11
View File
@@ -5,6 +5,13 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
android-test-dependencies:
patterns:
- "androidx.test.*"
update-types:
- minor
- patch
- package-ecosystem: npm
directory: /
schedule:
@@ -16,3 +23,7 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
+7 -6
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out source
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
@@ -31,22 +31,23 @@ jobs:
cache: gradle
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: pip
cache-dependency-path: tools/requirements.txt
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- name: Install Android SDK components
run: |
yes | sdkmanager --licenses >/dev/null || true
sdkmanager \
SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKMANAGER" --licenses >/dev/null || true
"$SDKMANAGER" \
'platforms;android-35' \
'build-tools;35.0.0' \
'ndk;29.0.14206865'
@@ -75,7 +76,7 @@ jobs:
- name: Upload APKs and reports
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: fridabox-${{ github.sha }}
if-no-files-found: warn
+2
View File
@@ -27,3 +27,5 @@ __pycache__/
*.pyc
node_modules/
.npm-cache/
/release-secrets/
/.release-work/
+1 -1
View File
@@ -26,7 +26,7 @@ android {
consumerProguardFiles "consumer-rules.pro"
ndk {
abiFilters 'arm64-v8a'
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
APP_ABI := armeabi-v7a arm64-v8a
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_PLATFORM := android-24
APP_STL := c++_static
APP_OPTIM := release
Binary file not shown.
Binary file not shown.
@@ -420,7 +420,7 @@ public class BActivityThread extends IBActivityThread.Stub {
Slog.w(TAG, "Unable to set guest context ClassLoader: " + error.getMessage());
}
}
FridaGadgetLoader.loadIfEnabled();
FridaGadgetLoader.loadAtProcessBind();
AppBindData bindData = new AppBindData();
bindData.appInfo = applicationInfo;
@@ -0,0 +1,169 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import android.os.Build;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import top.niunaijun.blackbox.BlackBoxCore;
/** Resolves a selected, downloaded Gadget and prepares per-guest runtime copies. */
final class DownloadedGadgetRuntime {
private static final String DOWNLOAD_ROOT = "fridabox-gadgets";
private static final String RUNTIME_ROOT = "fridabox-gadget-runtimes";
private static final String RUNTIME_NAME = "libpayload.so";
private static final String CONFIG_NAME = "libpayload.config.so";
private static final String SOURCE_NAME = "payload.source";
private DownloadedGadgetRuntime() {
}
static File prepareListener(String packageName) throws IOException {
Context context = BlackBoxCore.getContext();
File source = requireSelected(context);
File directory = new File(context.getFilesDir(),
RUNTIME_ROOT + File.separator + safeName(packageName));
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new IOException("Unable to create the Gadget runtime directory");
}
File runtime = new File(directory, RUNTIME_NAME);
syncSelected(source, runtime);
writeUtf8Atomically(new File(directory, CONFIG_NAME), listenerConfig());
return runtime;
}
static File requireSelected(Context context) throws IOException {
String selectedPath = InstrumentationSettings.getSelectedGadgetPath();
String selectedAbi = InstrumentationSettings.getSelectedGadgetAbi();
if (selectedPath == null || selectedPath.trim().isEmpty()) {
throw new IOException("No downloaded Frida Gadget is selected");
}
File root = new File(context.getFilesDir(), DOWNLOAD_ROOT).getCanonicalFile();
File selected = new File(selectedPath).getCanonicalFile();
if (!isInside(root, selected) || !selected.isFile()) {
throw new IOException("The selected Frida Gadget is missing or outside private storage");
}
if (!supportsAbi(selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI is not supported by this device");
}
validateElf(selected, selectedAbi);
return selected;
}
static void syncSelected(File source, File destination) throws IOException {
String identity = source.getCanonicalPath() + "\n"
+ String.valueOf(InstrumentationSettings.getSelectedGadgetSha256()) + "\n";
File marker = new File(destination.getParentFile(), SOURCE_NAME);
if (!destination.isFile() || destination.length() != source.length()
|| !identity.equals(readUtf8(marker))) {
copyAtomically(source, destination);
writeUtf8Atomically(marker, identity);
}
if (!destination.setReadable(true, false)
|| !destination.setExecutable(true, false)
|| !destination.setWritable(false, false)) {
throw new IOException("Unable to secure the private Frida Gadget runtime");
}
}
private static String listenerConfig() {
return "{\n" +
" \"interaction\": {\n" +
" \"type\": \"listen\",\n" +
" \"address\": \"127.0.0.1\",\n" +
" \"port\": " + InstrumentationSettings.getBasePort() + ",\n" +
" \"on_port_conflict\": \"pick-next\",\n" +
" \"on_load\": \"wait\"\n" +
" },\n" +
" \"runtime\": \"qjs\",\n" +
" \"teardown\": \"minimal\"\n" +
"}\n";
}
private static void validateElf(File file, String abi) throws IOException {
byte[] header = new byte[20];
try (FileInputStream input = new FileInputStream(file)) {
if (input.read(header) != header.length) throw new IOException("Downloaded Gadget is truncated");
}
if ((header[0] & 0xff) != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') {
throw new IOException("Downloaded Gadget is not an ELF library");
}
int machine = (header[18] & 0xff) | ((header[19] & 0xff) << 8);
int expected = expectedMachine(abi);
if (expected < 0 || machine != expected) {
throw new IOException("Downloaded Gadget ELF architecture does not match " + abi);
}
}
private static int expectedMachine(String abi) {
if ("arm64-v8a".equals(abi)) return 183;
if ("armeabi-v7a".equals(abi)) return 40;
if ("x86".equals(abi)) return 3;
if ("x86_64".equals(abi)) return 62;
return -1;
}
private static boolean supportsAbi(String abi) {
if (abi == null) return false;
for (String supported : Build.SUPPORTED_ABIS) {
if (abi.equals(supported)) return true;
}
return false;
}
private static boolean isInside(File root, File child) {
return child.getAbsolutePath().startsWith(root.getAbsolutePath() + File.separator);
}
private static String safeName(String value) {
return value == null ? "unknown" : value.replaceAll("[^A-Za-z0-9._-]", "_");
}
private static void copyAtomically(File source, File destination) throws IOException {
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
if (temporary.exists() && !temporary.delete()) throw new IOException("Unable to replace temporary Gadget");
try (FileInputStream input = new FileInputStream(source);
FileOutputStream output = new FileOutputStream(temporary)) {
byte[] buffer = new byte[128 * 1024];
int count;
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
output.getFD().sync();
}
replace(temporary, destination);
}
private static String readUtf8(File file) {
if (!file.isFile() || file.length() > 4096) return null;
try (FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream((int) file.length())) {
byte[] buffer = new byte[512];
int count;
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
return output.toString(StandardCharsets.UTF_8.name());
} catch (IOException ignored) {
return null;
}
}
static void writeUtf8Atomically(File destination, String value) throws IOException {
byte[] expected = value.getBytes(StandardCharsets.UTF_8);
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
try (FileOutputStream output = new FileOutputStream(temporary)) {
output.write(expected);
output.getFD().sync();
}
replace(temporary, destination);
}
private static void replace(File temporary, File destination) throws IOException {
if (destination.exists() && !destination.delete()) {
throw new IOException("Unable to replace " + destination.getName());
}
if (!temporary.renameTo(destination)) throw new IOException("Unable to install " + destination.getName());
}
}
@@ -15,19 +15,36 @@ public final class FridaGadgetLoader {
private FridaGadgetLoader() {
}
/** Listener mode starts immediately; autonomous scripts wait for the guest lifecycle. */
public static boolean loadAtProcessBind() {
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)) {
Log.i(TAG, "Deferring on-device agent until the guest application is ready");
return false;
}
return loadIfEnabled();
}
public static boolean loadIfEnabled() {
if (!GuestRuntimeRegistry.isInstrumentationEnabled()) {
Log.i(TAG, "Instrumentation disabled for this guest process");
return false;
}
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)
&& !GuestRuntimeRegistry.isPrimaryProcess()) {
Log.i(TAG, "Skipping on-device agent in secondary process "
+ GuestRuntimeRegistry.getGuestProcessName());
return false;
}
if (loaded) return true;
synchronized (LOAD_LOCK) {
if (loaded) return true;
if (!ATTEMPTED.compareAndSet(false, true)) return false;
try {
InstrumentationStatusStore.recordBinding();
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)) {
String scriptPath = InstrumentationSettings.getScriptPathForPackage(packageName);
File runtime = LocalScriptGadgetRuntime.prepare(packageName, scriptPath);
@@ -35,7 +52,8 @@ public final class FridaGadgetLoader {
System.load(runtime.getAbsolutePath());
} else {
Log.i(TAG, "Loading Frida Gadget listener for " + GuestRuntimeRegistry.getGuestProcessName());
System.loadLibrary("frida-gadget");
File runtime = DownloadedGadgetRuntime.prepareListener(packageName);
System.load(runtime.getAbsolutePath());
}
loaded = true;
InstrumentationStatusStore.recordLoaded();
@@ -56,6 +56,11 @@ public final class GuestRuntimeRegistry {
public static ClassLoader getGuestClassLoader() { return guestClassLoader; }
public static String getGuestSourceDir() { return guestSourceDir; }
public static boolean isInstrumentationEnabled() { return instrumentationEnabled; }
public static boolean isPrimaryProcess() {
String primaryProcess = guestApplicationInfo == null ? null : guestApplicationInfo.processName;
if (primaryProcess == null || primaryProcess.isEmpty()) primaryProcess = guestPackageName;
return primaryProcess != null && primaryProcess.equals(guestProcessName);
}
public static String getLastError() { return lastError; }
public static long getInitializationTimestamp() { return initializationTimestamp; }
@@ -81,6 +86,7 @@ public final class GuestRuntimeRegistry {
"\"virtualProcessId\":" + virtualProcessId + ',' +
"\"sourceDir\":" + quote(guestSourceDir) + ',' +
"\"instrumentationEnabled\":" + instrumentationEnabled + ',' +
"\"primaryProcess\":" + isPrimaryProcess() + ',' +
"\"lastError\":" + quote(lastError) + ',' +
"\"initializedAt\":" + initializationTimestamp +
'}';
@@ -12,6 +12,11 @@ public final class InstrumentationSettings {
public static final String KEY_BASE_PORT = "frida_base_port";
public static final String KEY_SCAN_COUNT = "frida_port_scan_count";
public static final String KEY_ADVANCED_LOGS = "show_advanced_logs";
public static final String KEY_GADGET_PATH = "selected_gadget_path";
public static final String KEY_GADGET_SOURCE = "selected_gadget_source";
public static final String KEY_GADGET_VERSION = "selected_gadget_version";
public static final String KEY_GADGET_ABI = "selected_gadget_abi";
public static final String KEY_GADGET_SHA256 = "selected_gadget_sha256";
private static final String PACKAGE_PREFIX = "package_enabled_";
private static final String PACKAGE_MODE_PREFIX = "package_mode_";
private static final String PACKAGE_SCRIPT_PREFIX = "package_script_";
@@ -97,6 +102,54 @@ public final class InstrumentationSettings {
return preferences().getBoolean(KEY_ADVANCED_LOGS, false);
}
public static String getSelectedGadgetPath() {
return preferences().getString(KEY_GADGET_PATH, null);
}
public static String getSelectedGadgetSource() {
return preferences().getString(KEY_GADGET_SOURCE, null);
}
public static String getSelectedGadgetVersion() {
return preferences().getString(KEY_GADGET_VERSION, null);
}
public static String getSelectedGadgetAbi() {
return preferences().getString(KEY_GADGET_ABI, null);
}
public static String getSelectedGadgetSha256() {
return preferences().getString(KEY_GADGET_SHA256, null);
}
public static boolean setSelectedGadget(
String path, String source, String version, String abi, String sha256) {
if (path == null || path.trim().isEmpty()
|| source == null || source.trim().isEmpty()
|| version == null || version.trim().isEmpty()
|| abi == null || abi.trim().isEmpty()
|| sha256 == null || sha256.trim().isEmpty()) {
return false;
}
return preferences().edit()
.putString(KEY_GADGET_PATH, path)
.putString(KEY_GADGET_SOURCE, source)
.putString(KEY_GADGET_VERSION, version)
.putString(KEY_GADGET_ABI, abi)
.putString(KEY_GADGET_SHA256, sha256)
.commit();
}
public static boolean clearSelectedGadget() {
return preferences().edit()
.remove(KEY_GADGET_PATH)
.remove(KEY_GADGET_SOURCE)
.remove(KEY_GADGET_VERSION)
.remove(KEY_GADGET_ABI)
.remove(KEY_GADGET_SHA256)
.commit();
}
static int clamp(int value, int minimum, int maximum, int fallback) {
return value < minimum || value > maximum ? fallback : value;
}
@@ -16,6 +16,7 @@ public final class InstrumentationStatusStore {
}
public static void recordBinding() {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
String state;
@@ -49,6 +50,7 @@ public final class InstrumentationStatusStore {
}
public static void recordLoaded() {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
String mode = preferences().getString("runtime_mode", InstrumentationSettings.MODE_COMPUTER);
String state = InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)
? "local_script_active" : "computer_attached";
@@ -56,6 +58,7 @@ public final class InstrumentationStatusStore {
}
public static void recordError(String error) {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
preferences().edit().putString("runtime_state", "failed").putString("runtime_error", error).commit();
}
}
@@ -3,6 +3,7 @@ package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -14,6 +15,8 @@ import top.niunaijun.blackbox.BlackBoxCore;
final class LocalScriptGadgetRuntime {
private static final String AGENT_ROOT = "fridabox-agents";
private static final String AGENT_NAME = "agent.js";
private static final String INSTRUMENTED_AGENT_NAME = "fridabox-runtime.js";
private static final String LOG_NAME = "runtime.jsonl";
private static final String RUNTIME_NAME = "libfridabox-agent.so";
private static final String CONFIG_NAME = "libfridabox-agent.config.so";
@@ -35,37 +38,80 @@ final class LocalScriptGadgetRuntime {
}
File directory = script.getParentFile();
File source = new File(context.getApplicationInfo().nativeLibraryDir, "libfrida-gadget.so");
if (!source.isFile()) throw new IOException("Packaged Frida Gadget is missing");
File log = new File(directory, LOG_NAME);
writeUtf8Atomically(log, "");
File instrumentedAgent = new File(directory, INSTRUMENTED_AGENT_NAME);
writeInstrumentedAgentAtomically(instrumentedAgent, script, buildLogBridge(log));
if (!instrumentedAgent.setReadable(true, true) || !instrumentedAgent.setWritable(false, false)) {
throw new IOException("Unable to secure the instrumented JavaScript agent");
}
File source = DownloadedGadgetRuntime.requireSelected(context);
File runtime = new File(directory, RUNTIME_NAME);
if (!runtime.isFile() || runtime.length() != source.length()) {
copyAtomically(source, runtime);
}
if (!runtime.setReadable(true, false)
|| !runtime.setExecutable(true, false)
|| !runtime.setWritable(false, false)) {
throw new IOException("Unable to secure private Frida Gadget permissions");
}
DownloadedGadgetRuntime.syncSelected(source, runtime);
File config = new File(directory, CONFIG_NAME);
writeUtf8Atomically(config, buildConfig(packageName));
writeUtf8Atomically(config, buildConfig(packageName, instrumentedAgent.getAbsolutePath()));
return runtime;
}
static String buildConfig(String packageName) {
static String buildConfig(String packageName, String agentPath) {
return "{\n" +
" \"interaction\": {\n" +
" \"type\": \"script\",\n" +
" \"path\": \"" + AGENT_NAME + "\",\n" +
" \"on_change\": \"reload\",\n" +
" \"path\": \"" + json(agentPath) + "\",\n" +
" \"on_change\": \"ignore\",\n" +
" \"parameters\": { \"package\": \"" + json(packageName) + "\" }\n" +
" },\n" +
" \"runtime\": \"qjs\",\n" +
" \"runtime\": \"v8\",\n" +
" \"teardown\": \"minimal\"\n" +
"}\n";
}
static String buildLogBridge(File log) {
return "(function () {\n" +
" try {\n" +
" const logPath = \"" + json(log.getAbsolutePath()) + "\";\n" +
" const maxBytes = 512 * 1024;\n" +
" const original = { log: console.log, warn: console.warn, error: console.error, send: globalThis.send };\n" +
" function render(value) {\n" +
" if (typeof value === 'string') return value;\n" +
" try { return JSON.stringify(value); } catch (_) { return String(value); }\n" +
" }\n" +
" function append(level, values) {\n" +
" try {\n" +
" const message = Array.prototype.map.call(values, render).join(' ');\n" +
" let line = JSON.stringify({ time: Date.now(), level: level, message: message }) + '\\n';\n" +
" let existing = '';\n" +
" try { existing = File.readAllText(logPath); } catch (_) {}\n" +
" if (existing.length + line.length > maxBytes) {\n" +
" const notice = JSON.stringify({ time: Date.now(), level: 'system', message: 'Earlier logs were truncated' }) + '\\n';\n" +
" const keep = Math.max(0, maxBytes - notice.length - line.length);\n" +
" existing = existing.slice(Math.max(0, existing.length - keep));\n" +
" line = notice + line;\n" +
" File.writeAllText(logPath, existing + line);\n" +
" return;\n" +
" }\n" +
" const stream = new File(logPath, 'a');\n" +
" try { stream.write(line); stream.flush(); } finally { stream.close(); }\n" +
" } catch (error) { try { original.error.call(console, '[FridaBox log bridge]', error.stack || error); } catch (_) {} }\n" +
" }\n" +
" console.log = function () { append('log', arguments); return original.log.apply(console, arguments); };\n" +
" console.warn = function () { append('warn', arguments); return original.warn.apply(console, arguments); };\n" +
" console.error = function () { append('error', arguments); return original.error.apply(console, arguments); };\n" +
" const wrappedSend = function (payload, data) { append('send', [payload]); return original.send(payload, data); };\n" +
" Object.defineProperty(globalThis, '__fridaboxSend', { value: wrappedSend, configurable: true });\n" +
" try {\n" +
" const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'send');\n" +
" if (!descriptor || descriptor.writable) globalThis.send = wrappedSend;\n" +
" else if (descriptor.configurable) Object.defineProperty(globalThis, 'send', Object.assign({}, descriptor, { value: wrappedSend }));\n" +
" } catch (error) { append('system', ['send() capture unavailable: ' + error]); }\n" +
" append('system', ['On-device agent started']);\n" +
" } catch (error) { try { console.error('[FridaBox log bridge]', error.stack || error); } catch (_) {} }\n" +
"})();\n";
}
static boolean isInside(File root, File child) {
String rootPath = root.getAbsolutePath();
String childPath = child.getAbsolutePath();
@@ -107,6 +153,101 @@ final class LocalScriptGadgetRuntime {
replace(temporary, destination);
}
private static void writeInstrumentedAgentAtomically(
File destination, File source, String bridge) throws IOException {
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
if (temporary.exists() && !temporary.delete()) {
throw new IOException("Unable to replace temporary JavaScript agent");
}
try (FileInputStream input = new FileInputStream(source);
ByteArrayOutputStream sourceBytes = new ByteArrayOutputStream((int) source.length())) {
byte[] buffer = new byte[64 * 1024];
int count;
while ((count = input.read(buffer)) >= 0) sourceBytes.write(buffer, 0, count);
byte[] instrumented = instrumentAgent(sourceBytes.toByteArray(), bridge);
try (FileOutputStream output = new FileOutputStream(temporary)) {
output.write(instrumented);
output.getFD().sync();
}
}
replace(temporary, destination);
}
static byte[] instrumentAgent(byte[] source, String bridge) throws IOException {
int offset = injectionOffset(source);
if (offset < 0) throw new IOException("Invalid Frida bundle header");
byte[] prefix = (bridge + "(function (send) {\n").getBytes(StandardCharsets.UTF_8);
byte[] suffix = "\n})(globalThis.__fridaboxSend);\n".getBytes(StandardCharsets.UTF_8);
if (offset > 0) return instrumentBundle(source, prefix, suffix, offset);
ByteArrayOutputStream output = new ByteArrayOutputStream(source.length + prefix.length + suffix.length);
output.write(prefix, 0, prefix.length);
output.write(source, 0, source.length);
output.write(suffix, 0, suffix.length);
return output.toByteArray();
}
private static byte[] instrumentBundle(
byte[] source, byte[] prefix, byte[] suffix, int bodyOffset) throws IOException {
int lengthStart = 0;
while (lengthStart < bodyOffset && source[lengthStart] != '\n') lengthStart++;
lengthStart++;
int lengthEnd = lengthStart;
long declaredLength = 0;
while (lengthEnd < bodyOffset && source[lengthEnd] >= '0' && source[lengthEnd] <= '9') {
declaredLength = declaredLength * 10 + (source[lengthEnd] - '0');
lengthEnd++;
}
if (lengthStart >= bodyOffset || lengthEnd == lengthStart
|| lengthEnd >= bodyOffset || source[lengthEnd] != ' ') {
throw new IOException("Invalid Frida bundle module length");
}
long bodyEndLong = bodyOffset + declaredLength;
if (bodyEndLong > source.length) throw new IOException("Invalid Frida bundle module length");
int bodyEnd = (int) bodyEndLong;
byte[] updatedLength = Long.toString(declaredLength + prefix.length + suffix.length)
.getBytes(StandardCharsets.US_ASCII);
ByteArrayOutputStream output = new ByteArrayOutputStream(
source.length + prefix.length + suffix.length
+ updatedLength.length - (lengthEnd - lengthStart));
output.write(source, 0, lengthStart);
output.write(updatedLength, 0, updatedLength.length);
output.write(source, lengthEnd, bodyOffset - lengthEnd);
output.write(prefix, 0, prefix.length);
output.write(source, bodyOffset, bodyEnd - bodyOffset);
output.write(suffix, 0, suffix.length);
output.write(source, bodyEnd, source.length - bodyEnd);
return output.toByteArray();
}
static int injectionOffset(byte[] source) {
byte[] magic = "📦".getBytes(StandardCharsets.UTF_8);
if (!startsWith(source, magic)) return 0;
byte[] unixMarker = "\n✄\n".getBytes(StandardCharsets.UTF_8);
int unix = indexOf(source, unixMarker);
if (unix >= 0) return unix + unixMarker.length;
byte[] windowsMarker = "\r\n✄\r\n".getBytes(StandardCharsets.UTF_8);
int windows = indexOf(source, windowsMarker);
return windows < 0 ? -1 : windows + windowsMarker.length;
}
private static boolean startsWith(byte[] value, byte[] prefix) {
if (value.length < prefix.length) return false;
for (int i = 0; i < prefix.length; i++) {
if (value[i] != prefix[i]) return false;
}
return true;
}
private static int indexOf(byte[] value, byte[] needle) {
for (int i = 0; i <= value.length - needle.length; i++) {
int j = 0;
while (j < needle.length && value[i + j] == needle[j]) j++;
if (j == needle.length) return i;
}
return -1;
}
private static void replace(File temporary, File destination) throws IOException {
if (destination.exists() && !destination.delete()) throw new IOException("Unable to replace " + destination.getName());
if (!temporary.renameTo(destination)) throw new IOException("Unable to install " + destination.getName());
@@ -43,10 +43,14 @@ public class AbiUtils {
String name = zipEntry.getName();
if (name.startsWith("lib/arm64-v8a")) {
mLibs.add("arm64-v8a");
} else if (name.startsWith("lib/armeabi")) {
mLibs.add("armeabi");
} else if (name.startsWith("lib/armeabi-v7a")) {
mLibs.add("armeabi-v7a");
} else if (name.startsWith("lib/armeabi")) {
mLibs.add("armeabi");
} else if (name.startsWith("lib/x86_64")) {
mLibs.add("x86_64");
} else if (name.startsWith("lib/x86")) {
mLibs.add("x86");
}
}
} catch (Exception e) {
@@ -57,11 +61,11 @@ public class AbiUtils {
}
public boolean is64Bit() {
return mLibs.contains("arm64-v8a");
return mLibs.contains("arm64-v8a") || mLibs.contains("x86_64");
}
public boolean is32Bit() {
return mLibs.contains("armeabi") || mLibs.contains("armeabi-v7a");
return mLibs.contains("armeabi") || mLibs.contains("armeabi-v7a") || mLibs.contains("x86");
}
public boolean isEmptyAib() {
@@ -25,8 +25,10 @@ public class NativeUtils {
nativeLibDir.mkdirs();
}
try (ZipFile zipfile = new ZipFile(apk.getAbsolutePath())) {
if (findAndCopyNativeLib(zipfile, Build.CPU_ABI, nativeLibDir)) {
return;
for (String abi : Build.SUPPORTED_ABIS) {
if (findAndCopyNativeLib(zipfile, abi, nativeLibDir)) {
return;
}
}
findAndCopyNativeLib(zipfile, "armeabi", nativeLibDir);
@@ -20,6 +20,7 @@ public class GuestRuntimeRegistryTest {
public void initializeReplacesProcessLocalSnapshot() {
ApplicationInfo info = new ApplicationInfo();
info.sourceDir = "/private/original.apk";
info.processName = "sample.one";
ClassLoader loader = getClass().getClassLoader();
GuestRuntimeRegistry.initialize("sample.one", "sample.one:remote", 3, 7, info, loader, true);
@@ -30,9 +31,19 @@ public class GuestRuntimeRegistryTest {
assertSame(loader, GuestRuntimeRegistry.getGuestClassLoader());
assertEquals("/private/original.apk", GuestRuntimeRegistry.getGuestSourceDir());
assertTrue(GuestRuntimeRegistry.isInstrumentationEnabled());
assertFalse(GuestRuntimeRegistry.isPrimaryProcess());
assertTrue(GuestRuntimeRegistry.describe().contains("\"package\":\"sample.one\""));
}
@Test
public void primaryProcessUsesApplicationProcessName() {
ApplicationInfo info = new ApplicationInfo();
info.processName = "sample.custom";
GuestRuntimeRegistry.initialize("sample", "sample.custom", 0, 1, info, null, true);
assertTrue(GuestRuntimeRegistry.isPrimaryProcess());
}
@Test
public void clearRemovesPriorGuest() {
GuestRuntimeRegistry.initialize("sample", "sample", 0, 1, null, null, true);
@@ -6,16 +6,54 @@ import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
public class LocalScriptGadgetRuntimeTest {
@Test
public void configUsesAutonomousScriptInteraction() {
String config = LocalScriptGadgetRuntime.buildConfig("sample.\"guest");
String config = LocalScriptGadgetRuntime.buildConfig(
"sample.\"guest", "/private/agents/fridabox-runtime.js");
assertTrue(config.contains("\"type\": \"script\""));
assertTrue(config.contains("\"path\": \"agent.js\""));
assertTrue(config.contains("\"path\": \"/private/agents/fridabox-runtime.js\""));
assertTrue(config.contains("\"on_change\": \"ignore\""));
assertTrue(config.contains("\"runtime\": \"v8\""));
assertTrue(config.contains("sample.\\\"guest"));
}
@Test
public void logBridgeCapturesConsoleAndSendWithoutChangingTheSourceAgent() {
String bridge = LocalScriptGadgetRuntime.buildLogBridge(new File("runtime.jsonl"));
assertTrue(bridge.contains("console.log = function"));
assertTrue(bridge.contains("console.error = function"));
assertTrue(bridge.contains("Object.getOwnPropertyDescriptor(globalThis, 'send')"));
assertTrue(bridge.contains("__fridaboxSend"));
assertTrue(bridge.contains("runtime.jsonl"));
assertTrue(bridge.contains("new File(logPath, 'a')"));
}
@Test
public void compiledBundleKeepsItsHeaderBeforeTheInjectedBridge() throws Exception {
String source = "📦\n16 /scripts/sample-hook.js\n✄\nvar hook = true;";
String bridge = "console.log('bridge');\n";
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
source.getBytes(StandardCharsets.UTF_8), bridge), StandardCharsets.UTF_8);
assertTrue(result.contains("✄\n" + bridge + "(function (send) {\nvar hook = true;"));
assertTrue(result.contains("})(globalThis.__fridaboxSend);"));
}
@Test
public void plainScriptReceivesBridgeAtTheBeginning() throws Exception {
String source = "console.log('agent');";
String bridge = "console.log('bridge');\n";
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
source.getBytes(StandardCharsets.UTF_8), bridge), StandardCharsets.UTF_8);
assertTrue(result.startsWith(bridge));
assertTrue(result.contains("(function (send) {\n" + source));
assertTrue(result.endsWith("})(globalThis.__fridaboxSend);\n"));
}
@Test
public void privatePathCheckRejectsSiblingPrefix() {
File root = new File("/data/user/0/host/files/fridabox-agents");
+15
View File
@@ -5,6 +5,21 @@ versioning for its public releases.
## [Unreleased]
## [4.1.0] - 2026-07-28
### Added
- Separate release APKs for `armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`.
- In-app APK inspection now accepts supported 32-bit ARM and x86 guest ABIs.
- Downloadable official Frida Gadget management, including ABI validation and
on-device or computer-controlled runtime selection.
- A liquid-glass launcher, downloadable Gadget screen, and guest log overlay.
### Changed
- Updated application iconography, navigation, runtime visibility, and release
dependency set.
### Documentation
- Added the public research roadmap for Frida visibility, `/proc`/maps
+15 -10
View File
@@ -1,5 +1,7 @@
<div align="center">
<img src="app/src/main/res/drawable-nodpi/logo.png" alt="FridaBox logo" width="180">
# FridaBox
### On-device Android instrumentation, inside a private virtual workspace
@@ -180,12 +182,12 @@ in On-device, Computer, and Clean modes. Evidence and exact commands are in
- JDK 21;
- Android SDK 35 and NDK 29.0.14206865;
- Python 3.10+;
- Node.js 20+ and npm when rebuilding the bundled Frida 17 agents.
- Node.js 20+ and npm for the pinned Frida agent toolchain and official Tabler
Icons source package.
### Build
```text
python tools/fetch_frida_gadget.py
npm ci
python tools/build_frida_agents.py
./gradlew :app:assembleDebug :Bcore:testDebugUnitTest :app:testDebugUnitTest :app:check
@@ -198,10 +200,12 @@ documented in [docs/BUILDING.md](docs/BUILDING.md). The output is written under
### Use
1. Install and open FridaBox.
2. Tap **Import APK** and select an ARM64 base APK.
3. Choose **On-device**, **Computer**, or **Clean** on that guest's card.
4. Select an agent for On-device mode, or attach normally in Computer mode.
5. Inspect live state in the **Runtime** tab.
2. Open **Gadgets** and download a compatible Official Frida version.
3. Select one downloaded Gadget as active.
4. Tap the import icon and choose an installed app or an ARM64 base APK file.
5. Tap the guest icon and choose **On-device**, **Computer**, or **Clean** in the launch panel.
6. Select an agent for On-device mode, or attach normally in Computer mode.
7. Inspect live state in the **Runtime** tab.
See [docs/USAGE.md](docs/USAGE.md) and
[docs/FRIDA_CONNECTION.md](docs/FRIDA_CONNECTION.md) for the complete workflow.
@@ -244,8 +248,9 @@ docs/ Build, usage, limitations and device evidence
## Scope and limitations
FridaBox currently targets single-file ARM64 APKs. Split APK sets, privileged or
system applications, full Google Play Services, Play Integrity, hardware-backed
FridaBox targets ARM64 APKs and can clone the active base/split set of a
launchable installed app. Standalone split archives, privileged system
applications, full Google Play Services, Play Integrity, hardware-backed
attestation, and perfect anti-instrumentation resistance are outside the current
scope. Vendor behavior and hidden Android APIs can still affect compatibility.
@@ -264,8 +269,8 @@ internally consistent and reducing avoidable instrumentation fingerprints:
- research safe mediation of `/proc`, including coherent views of process maps;
- improve filesystem, package, process, Binder, identity, ClassLoader, and
service behavior so guests observe a more faithful sandbox;
- replace the workable Codex-assisted UI baseline with a human-led product
design pass, an original FridaBox icon system, and a stronger visual identity;
- continue refining the FridaBox product identity and visual system around the
project logo;
- expand the Android/vendor test matrix and add regression probes for every
detection surface addressed.
+27
View File
@@ -42,6 +42,33 @@ FridaBox gratefully acknowledges the Frida maintainers and contributors for the
instrumentation engine, GumJS runtime, Java bridge, Gadget deployment model, and
controller APIs that make this research possible.
## Tabler Icons
FridaBox UI control icons are generated from the official `@tabler/icons`
package version 3.45.0, sourced from `https://github.com/tabler/tabler-icons`.
MIT License
Copyright (c) 2020-2026 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Frida Java bridge and agent compiler
The compiled JavaScript agents under `scripts/dist/` include
+28 -36
View File
@@ -32,7 +32,7 @@ android {
enable true
reset()
include "arm64-v8a"
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
@@ -77,7 +77,6 @@ android {
jniLibs {
excludes.add("**/libandroidx.graphics.path.so")
useLegacyPackaging true
keepDebugSymbols.add("**/libfrida-gadget.config.so")
}
resources {
excludes.add("**/libandroidx.graphics.path.so")
@@ -104,40 +103,20 @@ tasks.matching { it.name == "mergeDebugAssets" }.configureEach { dependsOn("copy
tasks.matching { it.name.toLowerCase().contains("debug") && it.name.toLowerCase().contains("lint") }
.configureEach { dependsOn("copyDemoGuestDebug") }
def fridaGadget = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.so")
def fridaGadgetConfig = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so")
tasks.register("verifyFridaGadget") {
inputs.files(fridaGadget, fridaGadgetConfig)
doLast {
if (!fridaGadget.isFile()) {
throw new GradleException("Frida Gadget 17.16.0 is missing. Run: python tools/fetch_frida_gadget.py")
}
if (!fridaGadgetConfig.isFile()) {
throw new GradleException("Frida Gadget configuration is missing: ${fridaGadgetConfig}")
}
def digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(fridaGadget.bytes).collect { String.format("%02x", it & 0xff) }.join()
if (digest != "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e") {
throw new GradleException("Frida Gadget 17.16.0 SHA-256 mismatch: ${digest}")
}
def config = new groovy.json.JsonSlurper().parse(fridaGadgetConfig)
if (config.interaction?.address != "127.0.0.1" || config.interaction?.on_load != "wait") {
throw new GradleException("Unsafe or invalid Frida Gadget configuration")
}
}
}
tasks.matching { it.name == "preBuild" }.configureEach { dependsOn("verifyFridaGadget") }
tasks.register("verifyDebugApkFridaPackaging") {
tasks.register("verifyDebugApkHasNoGadget") {
dependsOn("assembleDebug")
doLast {
def apk = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.singleFile
def entries = zipTree(apk).matching {
include("lib/arm64-v8a/libfrida-gadget.so")
include("lib/arm64-v8a/libfrida-gadget.config.so")
}.files.collect { it.name }.toSet()
if (!entries.contains("libfrida-gadget.so") || !entries.contains("libfrida-gadget.config.so")) {
throw new GradleException("Debug APK does not contain both Frida Gadget files")
def apks = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.files
if (apks.isEmpty()) {
throw new GradleException("No debug APKs were produced")
}
apks.each { apk ->
def entries = zipTree(apk).files.findAll {
it.name.toLowerCase().contains("gadget") && it.name.toLowerCase().endsWith(".so")
}
if (!entries.isEmpty()) {
throw new GradleException("${apk.name} must not bundle Frida Gadget: ${entries*.name}")
}
}
}
}
@@ -145,7 +124,7 @@ tasks.register("verifyDebugApkFridaPackaging") {
tasks.register("verifyInstrumentationOrdering") {
doLast {
def source = project(":Bcore").file("src/main/java/top/niunaijun/blackbox/app/BActivityThread.java").text
def loader = source.indexOf("FridaGadgetLoader.loadIfEnabled()")
def loader = source.indexOf("FridaGadgetLoader.loadAtProcessBind()")
def makeApplication = source.indexOf("makeApplication(", loader)
if (loader < 0 || makeApplication < 0 || loader >= makeApplication) {
throw new GradleException("Frida Gadget loader must execute before guest makeApplication")
@@ -153,6 +132,17 @@ tasks.register("verifyInstrumentationOrdering") {
}
}
tasks.register("verifyTablerAndroidIcons", Exec) {
workingDir(rootProject.projectDir)
def nodeExecutable = System.getProperty("os.name").toLowerCase().contains("windows") ? "node.exe" : "node"
commandLine(nodeExecutable, "tools/generate_tabler_android_icons.mjs", "--check")
doFirst {
if (!rootProject.file("node_modules/@tabler/icons/package.json").isFile()) {
throw new GradleException("Missing @tabler/icons. Run npm ci before building FridaBox.")
}
}
}
tasks.register("verifyDemoGuestUnmodified") {
dependsOn("copyDemoGuestDebug")
doLast {
@@ -166,7 +156,8 @@ tasks.register("verifyDemoGuestUnmodified") {
}
tasks.named("check").configure {
dependsOn("verifyDebugApkFridaPackaging", "verifyInstrumentationOrdering", "verifyDemoGuestUnmodified")
dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons")
}
dependencies {
@@ -176,6 +167,7 @@ dependencies {
implementation libs.material
implementation libs.constraintlayout
implementation libs.core.ktx
implementation "org.tukaani:xz:1.10"
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
+3 -2
View File
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -17,9 +18,9 @@
android:allowBackup="false"
android:extractNativeLibs="true"
android:fullBackupContent="false"
android:icon="@drawable/ic_fridabox_app"
android:icon="@mipmap/ic_launcher"
android:label="@string/fb_brand"
android:roundIcon="@drawable/ic_fridabox_app"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.FridaBox"
android:enableOnBackInvokedCallback="true"
@@ -2,6 +2,7 @@ package com.qm4rs.fridabox;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -13,24 +14,37 @@ public final class ApkInspector {
}
public static Result inspect(File apk) throws IOException {
return inspect(Collections.singletonList(apk));
}
public static Result inspect(File... apks) throws IOException {
return inspect(Arrays.asList(apks));
}
public static Result inspect(Iterable<File> apks) throws IOException {
Set<String> abis = new LinkedHashSet<>();
boolean hasNativeLibraries = false;
try (ZipFile archive = new ZipFile(apk)) {
if (archive.getEntry("AndroidManifest.xml") == null) {
throw new IOException("The selected file has no AndroidManifest.xml");
}
for (ZipEntry entry : Collections.list(archive.entries())) {
String name = entry.getName();
if (!entry.isDirectory() && name.startsWith("lib/") && name.endsWith(".so")) {
String[] parts = name.split("/", 3);
if (parts.length == 3) {
hasNativeLibraries = true;
abis.add(parts[1]);
boolean inspectedAny = false;
for (File apk : apks) {
inspectedAny = true;
try (ZipFile archive = new ZipFile(apk)) {
if (archive.getEntry("AndroidManifest.xml") == null) {
throw new IOException("The selected file has no AndroidManifest.xml");
}
for (ZipEntry entry : Collections.list(archive.entries())) {
String name = entry.getName();
if (!entry.isDirectory() && name.startsWith("lib/") && name.endsWith(".so")) {
String[] parts = name.split("/", 3);
if (parts.length == 3) {
hasNativeLibraries = true;
abis.add(parts[1]);
}
}
}
}
}
boolean supported = !hasNativeLibraries || abis.contains("arm64-v8a");
if (!inspectedAny) throw new IOException("No APK files were provided");
boolean supported = !hasNativeLibraries || !Collections.disjoint(abis, SUPPORTED_ABIS);
return new Result(hasNativeLibraries, supported, abis);
}
@@ -47,7 +61,11 @@ public final class ApkInspector {
public String description() {
if (!hasNativeLibraries) return "Pure Java/Kotlin (accepted)";
return supported ? "ARM64 supported: " + abis : "Rejected; no arm64-v8a: " + abis;
return supported ? "Supported ABI: " + abis : "Rejected; unsupported ABI: " + abis;
}
}
private static final Set<String> SUPPORTED_ABIS = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
"armeabi-v7a", "arm64-v8a", "x86", "x86_64"
)));
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
package com.qm4rs.fridabox
import android.app.Activity
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import org.json.JSONObject
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
/** Package-scoped floating log console shown inside a virtual guest Activity. */
object FridaBoxGuestLogOverlay {
private const val OVERLAY_TAG = "fridabox.guest.log.overlay"
private const val LOG_NAME = "runtime.jsonl"
private const val REFRESH_INTERVAL_MS = 750L
private val backgroundColor = Color.rgb(8, 13, 18)
private val surfaceColor = Color.rgb(18, 26, 34)
private val surfaceHighColor = Color.rgb(29, 40, 51)
private val primaryColor = Color.rgb(110, 231, 216)
private val textColor = Color.rgb(231, 238, 244)
private val secondaryTextColor = Color.rgb(148, 164, 178)
private val outlineColor = Color.rgb(49, 65, 80)
fun attach(activity: Activity, packageName: String) {
val decor = activity.window?.decorView as? ViewGroup ?: return
val existing = decor.findViewWithTag<View>(OVERLAY_TAG)
val localMode = InstrumentationSettings.MODE_LOCAL_SCRIPT ==
InstrumentationSettings.getModeForPackage(packageName)
val logFile = logFileFor(packageName)
if (!localMode || logFile == null) {
if (existing != null) decor.removeView(existing)
return
}
if (existing != null) {
existing.bringToFront()
return
}
val bubble = TextView(activity).apply {
tag = OVERLAY_TAG
text = ">_"
contentDescription = "Open Frida logs for $packageName"
gravity = Gravity.CENTER
setTextColor(Color.BLACK)
textSize = 15f
setTypeface(Typeface.MONOSPACE, Typeface.BOLD)
background = rounded(primaryColor, dp(activity, 18))
elevation = dp(activity, 10).toFloat()
setOnClickListener { showConsole(activity, packageName, logFile) }
}
val margin = dp(activity, 18)
decor.addView(bubble, FrameLayout.LayoutParams(dp(activity, 54), dp(activity, 54)).apply {
gravity = Gravity.END or Gravity.BOTTOM
marginEnd = margin
bottomMargin = dp(activity, 30)
})
makeDraggable(bubble, decor)
bubble.bringToFront()
}
fun show(activity: Activity, packageName: String): Boolean {
val logFile = logFileFor(packageName) ?: return false
showConsole(activity, packageName, logFile)
return true
}
private fun makeDraggable(view: View, parent: ViewGroup) {
val touchSlop = dp(view.context, 8).toFloat()
var downX = 0f
var downY = 0f
var startTranslationX = 0f
var startTranslationY = 0f
var dragged = false
view.setOnTouchListener { target, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
downX = event.rawX
downY = event.rawY
startTranslationX = target.translationX
startTranslationY = target.translationY
dragged = false
true
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX - downX
val dy = event.rawY - downY
dragged = dragged || kotlin.math.abs(dx) > touchSlop || kotlin.math.abs(dy) > touchSlop
val minX = -target.left.toFloat()
val maxX = (parent.width - target.right).toFloat()
val minY = -target.top.toFloat()
val maxY = (parent.height - target.bottom).toFloat()
target.translationX = (startTranslationX + dx).coerceIn(minX, maxX)
target.translationY = (startTranslationY + dy).coerceIn(minY, maxY)
true
}
MotionEvent.ACTION_UP -> {
if (!dragged) target.performClick()
true
}
MotionEvent.ACTION_CANCEL -> true
else -> false
}
}
}
private fun showConsole(activity: Activity, packageName: String, logFile: File) {
if (activity.isFinishing || activity.isDestroyed) return
val handler = Handler(Looper.getMainLooper())
val root = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(activity, 20), dp(activity, 20), dp(activity, 20), dp(activity, 16))
background = rounded(surfaceColor, dp(activity, 22))
}
root.addView(TextView(activity).apply {
text = "On-device agent logs"
textSize = 20f
setTypeface(typeface, Typeface.BOLD)
setTextColor(textColor)
})
val status = TextView(activity).apply {
text = packageName
textSize = 11.5f
setTextColor(secondaryTextColor)
setPadding(0, dp(activity, 4), 0, 0)
}
root.addView(status)
val output = TextView(activity).apply {
text = "No logs yet."
typeface = Typeface.MONOSPACE
textSize = 12f
setTextColor(textColor)
setTextIsSelectable(true)
setPadding(dp(activity, 14), dp(activity, 14), dp(activity, 14), dp(activity, 14))
background = rounded(backgroundColor, dp(activity, 12))
}
val scroll = ScrollView(activity).apply {
isFillViewport = true
addView(output, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
))
}
root.addView(scroll, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
).apply {
topMargin = dp(activity, 14)
bottomMargin = dp(activity, 14)
})
val actions = LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
}
var hiddenBefore = 0L
var fileSignature = Long.MIN_VALUE
val clear = button(activity, "Clear view", false) {
hiddenBefore = System.currentTimeMillis()
fileSignature = Long.MIN_VALUE
output.text = "No logs yet."
}
val copy = button(activity, "Copy", false) {
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("$packageName Frida logs", output.text))
Toast.makeText(activity, "Agent logs copied", Toast.LENGTH_SHORT).show()
}
lateinit var dialog: Dialog
val close = button(activity, "Close", true) { dialog.dismiss() }
actions.addView(clear, LinearLayout.LayoutParams(0, dp(activity, 44), 1f))
actions.addView(copy, LinearLayout.LayoutParams(0, dp(activity, 44), 1f).apply {
marginStart = dp(activity, 8)
})
actions.addView(close, LinearLayout.LayoutParams(0, dp(activity, 44), 1f).apply {
marginStart = dp(activity, 8)
})
root.addView(actions)
dialog = Dialog(activity).apply {
setContentView(root)
setCanceledOnTouchOutside(true)
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes = attributes.apply { dimAmount = 0.58f }
}
}
var snapshot = ""
lateinit var refresh: Runnable
refresh = Runnable {
if (!dialog.isShowing) return@Runnable
val length = logFile.length()
val nextSignature = length xor logFile.lastModified()
if (nextSignature != fileSignature) {
fileSignature = nextSignature
val next = formatLogs(logFile, hiddenBefore)
if (next != snapshot) {
val stayAtBottom = scroll.scrollY + scroll.height >= output.height - dp(activity, 24)
snapshot = next
output.text = next.ifBlank { "No logs yet." }
if (stayAtBottom) scroll.post { scroll.fullScroll(View.FOCUS_DOWN) }
}
val size = if (length in 1..1023) "<1 KiB" else "${length / 1024} KiB"
status.text = "$packageName · $size"
}
handler.postDelayed(refresh, REFRESH_INTERVAL_MS)
}
dialog.setOnShowListener {
val metrics = activity.resources.displayMetrics
dialog.window?.setLayout((metrics.widthPixels * 0.92f).toInt(), (metrics.heightPixels * 0.76f).toInt())
handler.post(refresh)
}
dialog.setOnDismissListener { handler.removeCallbacks(refresh) }
dialog.show()
}
private fun logFileFor(packageName: String): File? {
val scriptPath = InstrumentationSettings.getScriptPathForPackage(packageName) ?: return null
val directory = File(scriptPath).parentFile ?: return null
return File(directory, LOG_NAME)
}
private fun formatLogs(file: File, hiddenBefore: Long): String {
if (!file.isFile || file.length() == 0L) return ""
val formatter = SimpleDateFormat("HH:mm:ss.SSS", Locale.ROOT)
return runCatching {
file.readLines(Charsets.UTF_8).mapNotNull { line ->
runCatching<String?> {
val item = JSONObject(line)
val time = item.optLong("time")
if (time <= hiddenBefore) return@runCatching null
val timestamp = formatter.format(java.util.Date(time))
val level = item.optString("level", "log").uppercase(Locale.ROOT)
"[$timestamp] ${level.padEnd(6)} ${item.optString("message")}"
}.getOrElse { if (hiddenBefore == 0L) line else null }
}.joinToString("\n")
}.getOrElse { "Unable to read agent logs: ${it.message}" }
}
private fun button(context: Context, label: String, primary: Boolean, action: () -> Unit): TextView {
return TextView(context).apply {
text = label
gravity = Gravity.CENTER
textSize = 12f
setTypeface(typeface, Typeface.BOLD)
setTextColor(if (primary) Color.BLACK else textColor)
background = rounded(if (primary) primaryColor else surfaceHighColor, dp(context, 12),
if (primary) primaryColor else outlineColor)
setOnClickListener { action() }
}
}
private fun rounded(fill: Int, radius: Int, stroke: Int? = null): GradientDrawable {
return GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
setColor(fill)
cornerRadius = radius.toFloat()
if (stroke != null) setStroke(1, stroke)
}
}
private fun dp(context: Context, value: Int): Int =
(value * context.resources.displayMetrics.density).toInt()
}
@@ -1,16 +1,22 @@
package com.qm4rs.fridabox
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.io.File
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.app.BActivityThread
import top.niunaijun.blackbox.app.configuration.AppLifecycleCallback
import top.niunaijun.blackbox.app.configuration.ClientConfiguration
import top.niunaijun.blackbox.instrumentation.FridaGadgetLoader
import top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry
object FridaBoxRuntime {
private const val TAG = "FridaBox.Runtime"
private const val AGENT_READY_FALLBACK_MS = 5_000L
fun attach(context: Context) {
BlackBoxCore.get().doAttachBaseContext(context, object : ClientConfiguration() {
@@ -48,6 +54,19 @@ object FridaBoxRuntime {
userId: Int
) {
Log.d(TAG, "afterApplicationOnCreate: package=$packageName process=$processName")
if (GuestRuntimeRegistry.isPrimaryProcess()) {
Handler(Looper.getMainLooper()).postDelayed({
FridaGadgetLoader.loadIfEnabled()
}, AGENT_READY_FALLBACK_MS)
}
}
override fun onActivityResumed(activity: Activity) {
val packageName = runCatching { BActivityThread.getAppPackageName() }.getOrNull()
if (!packageName.isNullOrBlank()) {
FridaGadgetLoader.loadIfEnabled()
FridaBoxGuestLogOverlay.attach(activity, packageName)
}
}
override fun onStoragePermissionNeeded(packageName: String?, userId: Int): Boolean {
@@ -0,0 +1,382 @@
package com.qm4rs.fridabox
import android.content.Context
import android.os.Build
import org.json.JSONArray
import org.json.JSONObject
import org.tukaani.xz.XZInputStream
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.Locale
import java.util.zip.GZIPInputStream
enum class GadgetSource(
val id: String,
val title: String,
val repository: String
) {
OFFICIAL("official", "Official Frida", "frida/frida");
fun expectedAsset(version: String, architecture: String): String =
"frida-gadget-$version-android-$architecture.so.xz"
companion object {
fun fromId(value: String?): GadgetSource? = entries.firstOrNull { it.id == value }
}
}
data class GadgetAbi(val androidName: String, val releaseName: String) {
companion object {
private val supported = listOf(
GadgetAbi("arm64-v8a", "arm64"),
GadgetAbi("armeabi-v7a", "arm"),
GadgetAbi("x86_64", "x86_64"),
GadgetAbi("x86", "x86")
)
fun detect(): GadgetAbi? = Build.SUPPORTED_ABIS
.firstNotNullOfOrNull { deviceAbi -> supported.firstOrNull { it.androidName == deviceAbi } }
}
}
data class GadgetRelease(
val source: GadgetSource,
val version: String,
val abi: GadgetAbi,
val assetName: String,
val downloadUrl: String,
val compressedSize: Long,
val publishedAt: String
)
data class InstalledGadget(
val source: GadgetSource,
val version: String,
val abi: GadgetAbi,
val file: File,
val sha256: String,
val size: Long
) {
val identity: String get() = "${source.id}:$version:${abi.androidName}"
}
data class GadgetCatalog(
val releases: List<GadgetRelease>,
val fromCache: Boolean,
val hasMore: Boolean
)
class GadgetManager(private val context: Context) {
private val root = File(context.filesDir, DOWNLOAD_ROOT)
private val cacheRoot = File(context.cacheDir, "gadget-catalogs")
init {
if (InstrumentationSettings.getSelectedGadgetSource() != null
&& InstrumentationSettings.getSelectedGadgetSource() != GadgetSource.OFFICIAL.id) {
InstrumentationSettings.clearSelectedGadget()
}
}
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect()
fun loadCatalog(source: GadgetSource, abi: GadgetAbi, page: Int): GadgetCatalog {
val cache = File(cacheRoot, "${source.id}-${abi.androidName}-$page.json")
return try {
val json = requestText(releaseCatalogUrl(source, page))
val releases = parseCatalog(json, source, abi)
writeAtomically(cache, json.toByteArray(Charsets.UTF_8))
GadgetCatalog(releases, false, JSONArray(json).length() == CATALOG_PAGE_SIZE)
} catch (networkError: Exception) {
if (!cache.isFile) throw networkError
val cached = cache.readText(Charsets.UTF_8)
val releases = parseCatalog(cached, source, abi)
GadgetCatalog(releases, true, JSONArray(cached).length() == CATALOG_PAGE_SIZE)
}
}
fun download(release: GadgetRelease): InstalledGadget {
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
if (release.abi != currentAbi) throw IOException("The selected release does not match this device ABI")
val directory = installDirectory(release.source, release.version, release.abi).apply {
if (!isDirectory && !mkdirs()) throw IOException("Unable to create Gadget storage")
}
val temporary = File(directory, "$PAYLOAD_NAME.partial")
val destination = File(directory, PAYLOAD_NAME)
val metadata = File(directory, METADATA_NAME)
if (temporary.exists() && !temporary.delete()) throw IOException("Unable to replace partial download")
val connection = openConnection(release.downloadUrl, "application/octet-stream")
try {
if (connection.responseCode !in 200..299) {
throw IOException("GitHub download failed (HTTP ${connection.responseCode})")
}
val compressedLength = connection.contentLengthLong
if (compressedLength > MAX_COMPRESSED_BYTES) throw IOException("Gadget download is unexpectedly large")
val digest = MessageDigest.getInstance("SHA-256")
var total = 0L
decompressed(connection.inputStream, release.assetName).use { input ->
FileOutputStream(temporary).use { output ->
val buffer = ByteArray(128 * 1024)
while (true) {
val count = input.read(buffer)
if (count < 0) break
total += count
if (total > MAX_DECOMPRESSED_BYTES) throw IOException("Expanded Gadget is unexpectedly large")
digest.update(buffer, 0, count)
output.write(buffer, 0, count)
}
output.fd.sync()
}
}
if (total == 0L) throw IOException("Downloaded Gadget is empty")
validateElf(temporary, release.abi)
replace(temporary, destination)
if (!destination.setReadable(true, true) || !destination.setWritable(false, false)) {
throw IOException("Unable to secure downloaded Gadget")
}
val sha256 = digest.digest().joinToString("") { "%02x".format(it) }
val record = InstalledGadget(
release.source, release.version, release.abi, destination, sha256, total
)
writeMetadata(metadata, record, release.assetName)
return record
} catch (error: Exception) {
temporary.delete()
throw error
} finally {
connection.disconnect()
}
}
fun installed(): List<InstalledGadget> {
val abi = detectedAbi() ?: return emptyList()
if (!root.isDirectory) return emptyList()
return root.walkTopDown()
.filter { it.isFile && it.name == METADATA_NAME }
.mapNotNull { metadata -> runCatching { readMetadata(metadata) }.getOrNull() }
.filter { it.abi == abi && it.file.isFile }
.filter { runCatching { validateElf(it.file, it.abi) }.isSuccess }
.sortedWith(compareByDescending<InstalledGadget> { versionParts(it.version) }
.thenBy { it.source.ordinal })
.toList()
}
fun selected(): InstalledGadget? {
val path = InstrumentationSettings.getSelectedGadgetPath() ?: return null
val selected = runCatching { File(path).canonicalFile }.getOrNull() ?: return null
return installed().firstOrNull { runCatching { it.file.canonicalFile == selected }.getOrDefault(false) }
}
fun select(gadget: InstalledGadget) {
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
if (gadget.abi != currentAbi || !gadget.file.isFile) {
throw IOException("The selected Gadget is not available for this device")
}
validateElf(gadget.file, gadget.abi)
if (!InstrumentationSettings.setSelectedGadget(
gadget.file.canonicalPath,
gadget.source.id,
gadget.version,
gadget.abi.androidName,
gadget.sha256
)) {
throw IOException("Unable to save the selected Gadget")
}
}
fun findInstalled(release: GadgetRelease): InstalledGadget? {
val expected = runCatching {
File(installDirectory(release.source, release.version, release.abi), METADATA_NAME)
}.getOrNull() ?: return null
return runCatching { readMetadata(expected) }.getOrNull()
?.takeIf { it.file.isFile && runCatching { validateElf(it.file, it.abi) }.isSuccess }
}
private fun parseCatalog(json: String, source: GadgetSource, abi: GadgetAbi): List<GadgetRelease> {
val releases = JSONArray(json)
val result = ArrayList<GadgetRelease>()
for (index in 0 until releases.length()) {
val release = releases.optJSONObject(index) ?: continue
if (release.optBoolean("draft") || release.optBoolean("prerelease")) continue
val version = release.optString("tag_name").trim()
if (!isSafeSegment(version)) continue
val expected = source.expectedAsset(version, abi.releaseName)
val assets = release.optJSONArray("assets") ?: continue
for (assetIndex in 0 until assets.length()) {
val asset = assets.optJSONObject(assetIndex) ?: continue
val name = asset.optString("name")
if (name != expected) continue
val url = asset.optString("browser_download_url")
if (!isTrustedUrl(url)) continue
result += GadgetRelease(
source = source,
version = version,
abi = abi,
assetName = name,
downloadUrl = url,
compressedSize = asset.optLong("size", -1L),
publishedAt = release.optString("published_at")
)
break
}
}
return result
}
private fun installDirectory(source: GadgetSource, version: String, abi: GadgetAbi): File {
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
return File(root, "${source.id}${File.separator}$version${File.separator}${abi.androidName}")
}
private fun writeMetadata(file: File, gadget: InstalledGadget, assetName: String) {
val json = JSONObject()
.put("source", gadget.source.id)
.put("version", gadget.version)
.put("abi", gadget.abi.androidName)
.put("releaseAbi", gadget.abi.releaseName)
.put("sha256", gadget.sha256)
.put("size", gadget.size)
.put("asset", assetName)
.toString()
writeAtomically(file, json.toByteArray(Charsets.UTF_8))
}
private fun readMetadata(file: File): InstalledGadget {
if (!file.isFile || file.length() > MAX_METADATA_BYTES) throw IOException("Invalid Gadget metadata")
val json = JSONObject(file.readText(Charsets.UTF_8))
val source = GadgetSource.fromId(json.getString("source")) ?: throw IOException("Unknown Gadget source")
val version = json.getString("version")
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
val androidAbi = json.getString("abi")
val releaseAbi = json.getString("releaseAbi")
val abi = GadgetAbi(androidAbi, releaseAbi)
val payload = File(file.parentFile, PAYLOAD_NAME).canonicalFile
val canonicalRoot = root.canonicalFile
if (!payload.path.startsWith(canonicalRoot.path + File.separator)) throw IOException("Invalid Gadget path")
return InstalledGadget(
source, version, abi, payload, json.getString("sha256"), json.getLong("size")
)
}
private fun requestText(url: String): String {
val connection = openConnection(url, "application/vnd.github+json")
try {
if (connection.responseCode !in 200..299) {
throw IOException("GitHub release lookup failed (HTTP ${connection.responseCode})")
}
val bytes = readLimited(connection.inputStream, MAX_CATALOG_BYTES)
return bytes.toString(Charsets.UTF_8)
} finally {
connection.disconnect()
}
}
private fun openConnection(value: String, accept: String): HttpURLConnection {
if (!isTrustedUrl(value)) throw IOException("Untrusted Gadget download URL")
return (URL(value).openConnection() as HttpURLConnection).apply {
connectTimeout = 15_000
readTimeout = 45_000
instanceFollowRedirects = true
setRequestProperty("Accept", accept)
setRequestProperty("User-Agent", "FridaBox/${BuildConfig.VERSION_NAME}")
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
}
}
private fun decompressed(input: InputStream, assetName: String): InputStream = when {
assetName.endsWith(".xz") -> XZInputStream(input)
assetName.endsWith(".gz") -> GZIPInputStream(input, 128 * 1024)
else -> throw IOException("Unsupported Gadget archive format")
}
companion object {
private const val DOWNLOAD_ROOT = "fridabox-gadgets"
internal const val CATALOG_PAGE_SIZE = 10
private const val PAYLOAD_NAME = "payload.so"
private const val METADATA_NAME = "metadata.json"
private const val MAX_METADATA_BYTES = 16L * 1024L
private const val MAX_CATALOG_BYTES = 24L * 1024L * 1024L
private const val MAX_COMPRESSED_BYTES = 96L * 1024L * 1024L
private const val MAX_DECOMPRESSED_BYTES = 192L * 1024L * 1024L
internal fun releaseCatalogUrl(source: GadgetSource, page: Int): String {
require(page > 0) { "Catalog page must be positive" }
return "https://api.github.com/repos/${source.repository}/releases" +
"?per_page=$CATALOG_PAGE_SIZE&page=$page"
}
internal fun validateElf(file: File, abi: GadgetAbi) {
val header = ByteArray(20)
FileInputStream(file).use { input ->
if (input.read(header) != header.size) throw IOException("Downloaded Gadget is truncated")
}
if (header[0].toInt() and 0xff != 0x7f || header[1] != 'E'.code.toByte()
|| header[2] != 'L'.code.toByte() || header[3] != 'F'.code.toByte()) {
throw IOException("Downloaded Gadget is not an ELF library")
}
if (header[5].toInt() != 1) throw IOException("Only little-endian Android ELF libraries are supported")
val machine = (header[18].toInt() and 0xff) or ((header[19].toInt() and 0xff) shl 8)
val expected = when (abi.androidName) {
"arm64-v8a" -> 183
"armeabi-v7a" -> 40
"x86" -> 3
"x86_64" -> 62
else -> -1
}
if (machine != expected) throw IOException("Gadget ELF architecture does not match ${abi.androidName}")
}
private fun readLimited(input: InputStream, maximum: Long): ByteArray {
input.use {
val output = ByteArrayOutputStream()
val buffer = ByteArray(64 * 1024)
var total = 0L
while (true) {
val count = it.read(buffer)
if (count < 0) break
total += count
if (total > maximum) throw IOException("GitHub response is unexpectedly large")
output.write(buffer, 0, count)
}
return output.toByteArray()
}
}
private fun writeAtomically(destination: File, bytes: ByteArray) {
destination.parentFile?.let { if (!it.isDirectory && !it.mkdirs()) throw IOException("Unable to create storage") }
val temporary = File(destination.parentFile, destination.name + ".partial")
FileOutputStream(temporary).use { output ->
output.write(bytes)
output.fd.sync()
}
replace(temporary, destination)
}
private fun replace(temporary: File, destination: File) {
if (destination.exists() && !destination.delete()) throw IOException("Unable to replace ${destination.name}")
if (!temporary.renameTo(destination)) throw IOException("Unable to install ${destination.name}")
}
private fun isSafeSegment(value: String): Boolean =
value.isNotBlank() && value.length <= 64 && value.matches(Regex("[A-Za-z0-9._-]+"))
private fun isTrustedUrl(value: String): Boolean = runCatching {
val url = URL(value)
val host = url.host.lowercase(Locale.ROOT)
url.protocol == "https" && (host == "api.github.com" || host == "github.com"
|| host.endsWith(".githubusercontent.com"))
}.getOrDefault(false)
private fun versionParts(value: String): String = value.split('.')
.joinToString(".") { it.toIntOrNull()?.toString()?.padStart(8, '0') ?: it }
}
}
@@ -1,11 +0,0 @@
{
"interaction": {
"type": "listen",
"address": "127.0.0.1",
"port": 27042,
"on_port_conflict": "pick-next",
"on_load": "wait"
},
"runtime": "qjs",
"teardown": "minimal"
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<gradient android:angle="315" android:startColor="#1A2942" android:centerColor="#141D30" android:endColor="#111827" />
<corners android:radius="28dp" />
<stroke android:width="1dp" android:color="#334862" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="28dp" />
<gradient
android:angle="90"
android:startColor="#E0253850"
android:centerColor="#C91A2B42"
android:endColor="#B3122136" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="22dp" />
<gradient
android:angle="135"
android:startColor="#F02A3E58"
android:centerColor="#E01C2D45"
android:endColor="#D916263C" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="24dp" />
<gradient
android:angle="135"
android:startColor="#D9253850"
android:centerColor="#B51B2B42"
android:endColor="#9915253B" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#D564E8E8" android:endColor="#A329415D" />
<stroke android:width="1dp" android:color="#D9FFFFFF" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#A638516D" android:endColor="#7315263B" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
</item>
</selector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#59364E68" android:endColor="#33152235" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline_soft" />
</shape>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<gradient
android:angle="135"
android:startColor="#07111D"
android:centerColor="#11162D"
android:endColor="#080C18" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0.88"
android:centerY="0.08"
android:gradientRadius="420dp"
android:startColor="#5964E8E8"
android:centerColor="#2464E8E8"
android:endColor="#0064E8E8" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0.08"
android:centerY="0.92"
android:gradientRadius="460dp"
android:startColor="#4D9B87F5"
android:centerColor="#1F9B87F5"
android:endColor="#009B87F5" />
</shape>
</item>
</layer-list>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF05070B" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
</vector>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/apps.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 7l6 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M17 4l0 6" />
</vector>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/database-off.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 3l18 18" />
</vector>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/x.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M18 6l-12 12" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M6 6l12 12" />
</vector>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/info-circle.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 9h.01" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M11 12h1v4h1" />
</vector>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
</vector>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/package.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l0 9" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l-8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M16 5.25l-8 4.5" />
</vector>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file-import.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M5 13v-8a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-5.5m-9.5 -2h7m-3 -3l3 3l-3 3" />
</vector>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/trash.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 7l16 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M10 11l0 6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 11l0 6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</vector>
+13 -2
View File
@@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M3,13h4l2.4,-7.2L14,20l3,-7h4v-2h-5.3l-1.5,3.5L9.6,1L5.5,11H3z" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/activity.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 12h4l3 8l4 -16l3 8h4" />
</vector>
+20 -2
View File
@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M19.4,13a7.7,7.7 0,0 0,0.1 -1,7.7 7.7,0 0,0 -0.1,-1l2.1,-1.7 -2,-3.4 -2.5,1a8,8 0,0 0,-1.7 -1L15,3h-4l-0.4,2.8a8,8 0,0 0,-1.7 1l-2.5,-1 -2,3.4L6.6,11a7.7,7.7 0,0 0,-0.1 1,7.7 7.7,0 0,0 0.1,1l-2.1,1.7 2,3.4 2.5,-1a8,8 0,0 0,1.7 1L11,21h4l0.4,-2.8a8,8 0,0 0,1.7 -1l2.5,1 2,-3.4zM13,15.5a3.5,3.5 0,1 1,0 -7,3.5 3.5,0 0,1 0,7z" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/settings.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</vector>
+34 -2
View File
@@ -1,4 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M3,3h8v8H3zM13,3h8v5h-8zM13,10h8v11h-8zM3,13h8v8H3z" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/layout-grid.svg -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
</vector>
+72 -26
View File
@@ -4,15 +4,18 @@
android:id="@+id/fridabox_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/fb_background"
android:background="@drawable/bg_liquid_glass_app"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/fb_background"
android:elevation="8dp"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -20,16 +23,56 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="@color/fb_background"
android:contentInsetStart="18dp"
android:contentInsetEnd="18dp"
app:logo="@drawable/ic_fridabox_mark"
app:logoDescription="@string/fb_brand"
app:subtitle="@string/fb_brand_tagline"
app:subtitleTextColor="@color/fb_text_secondary"
app:title="@string/fb_brand"
app:titleTextColor="@color/fb_text_primary" />
android:layout_height="64dp"
android:background="@drawable/bg_glass_toolbar"
android:elevation="10dp"
android:contentInsetStart="0dp"
android:contentInsetEnd="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layoutDirection="ltr"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="14dp">
<ImageView
android:layout_width="44dp"
android:layout_height="44dp"
android:contentDescription="@string/fb_brand"
android:scaleType="centerInside"
android:src="@drawable/logo" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@string/fb_brand"
android:textColor="@color/fb_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/fb_brand_tagline"
android:textColor="@color/fb_text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
@@ -79,29 +122,32 @@
app:layout_constraintTop_toBottomOf="@id/app_bar"
app:trackColor="@color/fb_surface" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/import_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/import_fab_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
android:letterSpacing="0"
android:text="@string/fb_import_apk"
android:textAllCaps="false"
android:textColor="@color/fb_black"
android:layout_marginBottom="14dp"
android:contentDescription="@string/fb_import"
app:backgroundTint="@color/fb_primary"
app:icon="@drawable/ic_fb_add"
app:iconTint="@color/fb_black"
app:elevation="12dp"
app:srcCompat="@drawable/ic_fb_import"
app:tint="@color/fb_black"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="72dp"
android:background="@color/fb_surface"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_glass_navigation"
android:elevation="14dp"
app:itemActiveIndicatorStyle="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
app:itemIconTint="@color/fb_text_primary"
app:itemRippleColor="@color/fb_glass_highlight"
app:itemTextColor="@color/fb_text_primary"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace" android:title="@string/fb_nav_workspace" />
<item android:id="@+id/nav_gadgets" android:icon="@drawable/ic_fb_gadget" android:title="@string/fb_nav_gadgets" />
<item android:id="@+id/nav_runtime" android:icon="@drawable/ic_fb_runtime" android:title="@string/fb_nav_runtime" />
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings" android:title="@string/fb_nav_settings" />
</menu>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+11 -5
View File
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="fb_background">#080C14</color>
<color name="fb_surface">#111827</color>
<color name="fb_surface_high">#182235</color>
<color name="fb_surface_tint">#1D2940</color>
<color name="fb_background">#07111D</color>
<color name="fb_surface">#18263A</color>
<color name="fb_surface_high">#22344D</color>
<color name="fb_surface_tint">#29415D</color>
<color name="fb_glass_surface">#B31A2940</color>
<color name="fb_glass_surface_strong">#E0203047</color>
<color name="fb_glass_surface_soft">#731C3049</color>
<color name="fb_glass_outline">#66FFFFFF</color>
<color name="fb_glass_outline_soft">#2EFFFFFF</color>
<color name="fb_glass_highlight">#38FFFFFF</color>
<color name="fb_primary">#64E8E8</color>
<color name="fb_primary_dark">#1AAFB5</color>
<color name="fb_secondary">#9B87F5</color>
<color name="fb_text_primary">#F8FAFC</color>
<color name="fb_text_secondary">#9CAEC5</color>
<color name="fb_outline">#2B3A52</color>
<color name="fb_outline">#52708F</color>
<color name="fb_success">#42D39A</color>
<color name="fb_warning">#F5BF5B</color>
<color name="fb_error">#FB7185</color>
+30 -8
View File
@@ -3,16 +3,17 @@
<string name="fb_brand">FridaBox</string>
<string name="fb_brand_tagline">Mobile instrumentation workspace</string>
<string name="fb_nav_workspace">Workspace</string>
<string name="fb_nav_gadgets">Gadgets</string>
<string name="fb_nav_runtime">Runtime</string>
<string name="fb_nav_settings">Settings</string>
<string name="fb_import_apk">Import APK</string>
<string name="fb_hero_eyebrow">PRIVATE · NON-ROOT · ARM64</string>
<string name="fb_hero_title">Instrument apps. Keep control.</string>
<string name="fb_hero_body">Run trusted JavaScript agents directly on the device, connect from a computer, or launch a clean guest—per app.</string>
<string name="fb_guest_workspace">Guest workspace</string>
<string name="fb_guest_workspace_hint">Each guest keeps its own launch mode and agent.</string>
<string name="fb_no_guests">Your workspace is empty</string>
<string name="fb_no_guests_body">Import one ARM64 base APK to create a private FridaBox guest.</string>
<string name="fb_import">Import</string>
<string name="fb_import_from_apps">Import from apps</string>
<string name="fb_import_from_file">Import from file</string>
<string name="fb_installed_apps">Installed apps</string>
<string name="fb_import_app">Import</string>
<string name="fb_app_imported">Imported</string>
<string name="fb_loading_apps">Loading apps…</string>
<string name="fb_no_importable_apps">No importable apps found</string>
<string name="fb_mode_local">On-device</string>
<string name="fb_mode_computer">Computer</string>
<string name="fb_mode_clean">Clean</string>
@@ -56,4 +57,25 @@
<string name="fb_remove_body">The virtual app, its virtual data, and its saved on-device agent will be removed from FridaBox. The Android PackageManager installation is not changed.</string>
<string name="fb_remove">Remove</string>
<string name="fb_details">App details</string>
<string name="fb_gadgets_title">Gadget manager</string>
<string name="fb_active_gadget">Active Gadget</string>
<string name="fb_no_active_gadget_short">Not selected</string>
<string name="fb_installed_gadgets">Installed</string>
<string name="fb_browse_gadgets">Browse versions</string>
<string name="fb_close">Close</string>
<string name="fb_loading_versions">Loading versions…</string>
<string name="fb_all_versions_loaded">All available versions loaded</string>
<string name="fb_cached_versions">Showing cached versions</string>
<string name="fb_use_gadget">Use this Gadget</string>
<string name="fb_gadget_active">Active</string>
<string name="fb_download">Download</string>
<string name="fb_gadget_required">Download and select a Gadget before using an instrumented launch mode.</string>
<string name="fb_gadget_selected">Gadget %1$s selected</string>
<string name="fb_gadget_select_failed">Unable to select Gadget</string>
<string name="fb_gadget_downloading">Downloading Gadget %1$s…</string>
<string name="fb_gadget_downloaded">Gadget %1$s downloaded. Choose it from Installed when you want to use it.</string>
<string name="fb_gadget_downloaded_selected">Gadget %1$s downloaded and selected.</string>
<string name="fb_gadget_download_failed">Gadget download failed: %1$s</string>
<string name="fb_catalog_failed_body">Unable to load compatible Gadget releases from GitHub.</string>
<string name="fb_gadget_unsupported_title">Unsupported device ABI</string>
</resources>
+3 -3
View File
@@ -11,8 +11,8 @@
<item name="colorOnSurface">@color/fb_text_primary</item>
<item name="colorError">@color/fb_error</item>
<item name="android:colorAccent">@color/fb_primary</item>
<item name="android:windowBackground">@color/fb_background</item>
<item name="android:navigationBarColor">@color/fb_surface</item>
<item name="android:windowBackground">@drawable/bg_liquid_glass_app</item>
<item name="android:navigationBarColor">@color/fb_background</item>
<item name="android:statusBarColor">@color/fb_background</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
@@ -22,7 +22,7 @@
</style>
<style name="ThemeOverlay.FridaBox.Dialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="colorSurface">@color/fb_surface_high</item>
<item name="colorSurface">@color/fb_glass_surface_strong</item>
<item name="colorOnSurface">@color/fb_text_primary</item>
<item name="colorPrimary">@color/fb_primary</item>
</style>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0A0A0A</color>
</resources>
@@ -31,10 +31,26 @@ public class ApkInspectorTest {
}
@Test
public void thirtyTwoBitOnlyApkIsRejected() throws Exception {
public void thirtyTwoBitOnlyApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/armeabi-v7a/libsample.so"));
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
assertTrue(result.supported);
}
@Test
public void x86ApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/x86/libsample.so"));
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void splitNativeLibrariesAreInspectedTogether() throws Exception {
File base = apkWith("classes.dex");
File arm64Split = apkWith("lib/arm64-v8a/libsample.so");
ApkInspector.Result result = ApkInspector.inspect(base, arm64Split);
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
private File apkWith(String entry) throws Exception {
@@ -0,0 +1,56 @@
package com.qm4rs.fridabox
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.IOException
class GadgetManagerTest {
@get:Rule
val temporary = TemporaryFolder()
@Test
fun releaseAssetNameMatchesOfficialRepository() {
assertEquals(
"frida-gadget-17.9.11-android-arm64.so.xz",
GadgetSource.OFFICIAL.expectedAsset("17.9.11", "arm64")
)
}
@Test
fun releaseCatalogUsesTenItemPages() {
assertEquals(
"https://api.github.com/repos/frida/frida/releases?per_page=10&page=3",
GadgetManager.releaseCatalogUrl(GadgetSource.OFFICIAL, 3)
)
}
@Test
fun elfValidationAcceptsMatchingArchitecture() {
val file = temporary.newFile("payload.so")
file.writeBytes(elfHeader(183))
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
}
@Test
fun elfValidationRejectsWrongArchitecture() {
val file = temporary.newFile("payload.so")
file.writeBytes(elfHeader(40))
assertThrows(IOException::class.java) {
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
}
}
private fun elfHeader(machine: Int): ByteArray = ByteArray(20).also {
it[0] = 0x7f
it[1] = 'E'.code.toByte()
it[2] = 'L'.code.toByte()
it[3] = 'F'.code.toByte()
it[4] = 2
it[5] = 1
it[18] = (machine and 0xff).toByte()
it[19] = (machine shr 8).toByte()
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ ext {
targetSdkVersion = 28
minSdk = 21
versionCode = 400
versionName = "4.0.0"
versionCode = 401
versionName = "4.1.0"
xVersion = '1.1.0'
hiddenApiBypass = '4.3'
}
+17 -14
View File
@@ -7,23 +7,19 @@
- Android NDK 29.0.13846066 when available. This build host only had the closely
related complete NDK 29.0.14206865, which is recorded in `docs/BASELINE.md`.
- Python 3.10 or newer.
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents.
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents
and generating Android vectors from the official `@tabler/icons` package.
Create `local.properties` with the SDK path, then fetch the pinned official
Gadget:
```powershell
python tools\fetch_frida_gadget.py
```
The script uses the GitHub Releases API and Python's standard `lzma` module. The
build fails with a direct instruction if the binary is absent. No XZ archive is
kept.
Create `local.properties` with the SDK path. Frida Gadget is intentionally not a
build input and no Gadget binary is packaged in the APK. After installation, the
user downloads and selects an ABI-compatible Official Frida build from the
in-app **Gadgets** screen.
Install the pinned Java bridge/compiler and build the controller agents:
```powershell
npm ci
npm run verify:android-icons
python tools\build_frida_agents.py
```
@@ -31,6 +27,10 @@ Frida 17 no longer bundles runtime bridges into API-loaded GumJS agents. The
compiled files under `scripts/dist/` are reproducible from the source scripts,
`package.json`, and `package-lock.json`.
UI control icons are generated from pinned `@tabler/icons` sources with
`npm run generate:android-icons`. The app `check` task runs the matching
verification script and fails if a committed Android vector is out of sync.
On Windows, if the repository path contains spaces, create a no-space junction
and set the Bcore-only native path override:
@@ -51,7 +51,10 @@ Build and test:
The debug host build depends on the sample build and copies its byte-identical
APK into generated debug assets. Generated sample APKs are not source-controlled.
The final host output is under `app/build/outputs/apk/debug/` and is ARM64-only.
The final host outputs are under `app/build/outputs/apk/debug/` for `armeabi-v7a`,
`arm64-v8a`, `x86`, and `x86_64`.
`verifyDebugApkHasNoGadget` inspects the assembled APK and fails if a Gadget
library or configuration is accidentally bundled again.
## Production release
@@ -80,5 +83,5 @@ If only some signing variables are present, configuration fails instead of
producing an ambiguously signed artifact. Keep the keystore and credentials
outside the repository and CI logs.
The ARM64 release output is written to
`app/build/outputs/apk/release/FridaBox_4.0.0_arm64-v8a-release.apk`.
Release outputs are written to `app/build/outputs/apk/release/`, one APK each for
`armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`.
+11 -11
View File
@@ -1,6 +1,6 @@
# Test results
Validation date: 2026-07-19
Validation date: 2026-07-27
## Host validation
@@ -12,10 +12,10 @@ The following commands completed successfully on the build host:
.\gradlew.bat :app:assembleDebug
.\gradlew.bat test
.\gradlew.bat :app:check
.\gradlew.bat :app:verifyDebugApkFridaPackaging :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
.\gradlew.bat :app:verifyDebugApkHasNoGadget :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
npm ci
python tools\build_frida_agents.py
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\fetch_frida_gadget.py tools\build_frida_agents.py
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\build_frida_agents.py
```
For this workspace path, native builds used:
@@ -24,8 +24,12 @@ For this workspace path, native builds used:
$env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'
```
Unit-test results: 16 executions passed, with zero failures, errors, or skips.
This is eight test methods run for both debug and release variants:
Unit tests passed with zero failures, including Gadget asset naming, ten-item
release pagination, ELF ABI validation, and combined base/split APK inspection.
The original suite also covers:
The app `check` task also verifies that all committed UI control vectors are
byte-for-byte reproducible from pinned `@tabler/icons` 3.45.0 sources.
- `ApkInspectorTest`: 3 per variant;
- `ApkIntegrityTest`: 1 per variant;
@@ -35,13 +39,9 @@ This is eight test methods run for both debug and release variants:
The final APK and custom verification tasks passed:
- output: `app/build/outputs/apk/debug/FridaBox_4.0.0_arm64-v8a-debug.apk`;
- size: 19,977,904 bytes;
- SHA-256: `a87af38ff7f4a54d2cdc0207ac68319a1a05ab8067b7acdeb057c6c967ed2573`;
- packaged ABIs: ARM64 only;
- packaged native files: `libblackbox.so`, `libfrida-gadget.so`, and
`libfrida-gadget.config.so` under `lib/arm64-v8a/`;
- Frida Gadget 17.16.0 SHA-256:
`6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e`;
- packaged native files do not include a Frida Gadget binary or
Gadget configuration;
- early-load ordering and byte-identical demo-asset checks passed.
- pinned Frida 17 registry/bootstrap/sample agents rebuilt successfully, and
controller help plus empty-range discovery completed successfully.
+25 -7
View File
@@ -5,12 +5,30 @@ Importing an APK does not install that package into Android's real PackageManage
## Import an application
1. Open **Workspace** and tap **Import APK**.
2. Select one ARM64 base `.apk` through Android's document picker.
3. FridaBox validates the APK, records its SHA-256, and creates a private guest.
1. Open **Workspace** and tap the import icon.
2. Choose **Import from apps** to clone a launchable app already installed on
the device, or **Import from file** to select one base `.apk` through
Android's document picker.
3. FridaBox validates the full installed base/split set or selected file,
records its SHA-256 fingerprint, and creates a private guest.
Split-only packages (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs
are rejected.
Split-only files (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs are
rejected. Installed split applications are imported through their system package
so their active base and split APKs remain available to the virtual runtime.
## Install a Gadget
1. Open **Gadgets**. FridaBox detects the device ABI and only offers matching
release assets.
2. Tap **Browse versions**. The minimal release browser loads Official builds
from `frida/frida` ten at a time as the list is scrolled.
3. Download any listed version. Downloads are decompressed in private storage,
checked as ELF libraries, and rejected when their architecture does not match.
4. Select any installed version as active. Downloading another version does not
replace the active selection unless no Gadget had been selected yet.
Instrumented launches require an active Gadget. Clean mode remains available
without one. Gadget binaries are never bundled in the FridaBox APK.
## Choose a launch mode
@@ -34,7 +52,7 @@ Switching modes always stops the previous guest process before the next launch.
## On-device agents
Select **On-device**, tap **Select JavaScript**, review the trust warning, and
Tap an app icon, select **On-device**, tap **Select JavaScript**, review the trust warning, and
choose a `.js` file up to 16 MiB. The original file is not modified. The private
copy and the private Gadget executable are made read-only before execution.
@@ -46,7 +64,7 @@ interceptors, and in-app overlays continue to run in the guest process.
The **Runtime** tab reports the latest package, process, virtual user ID, virtual
process slot, source APK, ClassLoader, selected mode, state, and latest error.
Use **Manage** on an app card for app details, runtime details, virtual data
Use **Manage** in an app's launch panel for app details, runtime details, virtual data
clearing, or removal from the private workspace.
Only run JavaScript agents you trust. An on-device agent executes with the same
+3 -4
View File
@@ -1,12 +1,12 @@
[versions]
agp = "8.13.2"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.0"
material = "1.12.0"
constraintlayout = "2.2.0"
kotlin = "1.9.23"
kotlin = "2.4.10"
coreKtx = "1.15.0"
[libraries]
@@ -22,4 +22,3 @@ core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx"
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
+12
View File
@@ -9,9 +9,21 @@
"frida-java-bridge": "7.0.13"
},
"devDependencies": {
"@tabler/icons": "3.45.0",
"frida-compile": "19.0.5"
}
},
"node_modules/@tabler/icons": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.45.0.tgz",
"integrity": "sha512-jiATwV8+zGYLTZ7gMLGivCic+KtsMZXcDmufIG8umlLxoHhI6902hGYIEt0Oa9Y9SXblNzUlrisHm5jOFMxOQA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+5
View File
@@ -1,10 +1,15 @@
{
"name": "fridabox-agents",
"private": true,
"scripts": {
"generate:android-icons": "node tools/generate_tabler_android_icons.mjs",
"verify:android-icons": "node tools/generate_tabler_android_icons.mjs --check"
},
"dependencies": {
"frida-java-bridge": "7.0.13"
},
"devDependencies": {
"@tabler/icons": "3.45.0",
"frida-compile": "19.0.5"
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId "com.qm4rs.fridabox.sample"
minSdk rootProject.ext.minSdk
targetSdk rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
}
compileOptions {
@@ -3,6 +3,7 @@ package com.qm4rs.fridabox.sample;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -16,7 +17,12 @@ public final class MainActivity extends Activity {
output.setText("Press the button to call Target.add(2, 3)");
Button button = new Button(this);
button.setText("Call Target.add(2, 3)");
button.setOnClickListener(view -> output.setText("Result: " + Target.add(2, 3)));
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
output.setText("Result: " + Target.add(2, 3));
}
});
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setGravity(Gravity.CENTER);
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""Fetch and verify the pinned official Frida Gadget Android ARM64 binary."""
from __future__ import annotations
import argparse
import hashlib
import json
import lzma
import pathlib
import shutil
import tempfile
import urllib.request
VERSION = "17.16.0"
ASSET = f"frida-gadget-{VERSION}-android-arm64.so.xz"
API_URL = f"https://api.github.com/repos/frida/frida/releases/tags/{VERSION}"
ROOT = pathlib.Path(__file__).resolve().parents[1]
DEST = ROOT / "app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so"
CHECKSUM = ROOT / "tools/frida-gadget-17.16.0.sha256"
EXPECTED_SHA256 = "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e"
def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def release_asset_url() -> str:
request = urllib.request.Request(
API_URL,
headers={"Accept": "application/vnd.github+json", "User-Agent": "FridaBox-build"},
)
with urllib.request.urlopen(request, timeout=30) as response:
release = json.load(response)
if release.get("tag_name") != VERSION:
raise RuntimeError(f"GitHub returned unexpected release {release.get('tag_name')!r}")
for asset in release.get("assets", []):
if asset.get("name") == ASSET:
return str(asset["browser_download_url"])
raise RuntimeError(f"Release {VERSION} does not contain {ASSET}")
def fetch(force: bool = False) -> str:
if DEST.exists() and not force:
digest = sha256(DEST)
if digest != EXPECTED_SHA256:
raise RuntimeError(f"Existing Gadget SHA-256 mismatch: {digest}")
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
return digest
DEST.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="fridabox-") as temp_dir:
archive = pathlib.Path(temp_dir) / ASSET
request = urllib.request.Request(release_asset_url(), headers={"User-Agent": "FridaBox-build"})
with urllib.request.urlopen(request, timeout=120) as response, archive.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_output = pathlib.Path(temp_dir) / DEST.name
with lzma.open(archive, "rb") as source, temporary_output.open("wb") as output:
shutil.copyfileobj(source, output)
if temporary_output.stat().st_size < 1_000_000:
raise RuntimeError("Downloaded Gadget is unexpectedly small")
shutil.move(str(temporary_output), DEST)
digest = sha256(DEST)
if digest != EXPECTED_SHA256:
DEST.unlink(missing_ok=True)
raise RuntimeError(f"Downloaded Gadget SHA-256 mismatch: {digest}")
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
return digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--force", action="store_true", help="replace an existing Gadget")
args = parser.parse_args()
digest = fetch(args.force)
print(f"Frida Gadget {VERSION}: {DEST}")
print(f"SHA-256: {digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-1
View File
@@ -1 +0,0 @@
6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e libfrida-gadget.so
+69
View File
@@ -0,0 +1,69 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const packageRoot = path.join(projectRoot, 'node_modules', '@tabler', 'icons');
const drawableRoot = path.join(projectRoot, 'app', 'src', 'main', 'res', 'drawable');
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
const checkOnly = process.argv.includes('--check');
const icons = new Map([
['layout-grid', 'ic_fb_workspace.xml'],
['package', 'ic_fb_gadget.xml'],
['activity', 'ic_fb_runtime.xml'],
['settings', 'ic_fb_settings.xml'],
['file-import', 'ic_fb_import.xml'],
['apps', 'ic_fb_apps.xml'],
['file', 'ic_fb_file.xml'],
['x', 'ic_fb_close.xml'],
['info-circle', 'ic_fb_details.xml'],
['database-off', 'ic_fb_clear.xml'],
['trash', 'ic_fb_remove.xml'],
]);
let mismatches = 0;
for (const [iconName, outputName] of icons) {
const sourcePath = path.join(packageRoot, 'icons', 'outline', `${iconName}.svg`);
const svg = fs.readFileSync(sourcePath, 'utf8');
const paths = [...svg.matchAll(/<path\s+d="([^"]+)"\s*\/>/g)].map((match) => match[1]);
if (paths.length === 0) throw new Error(`No SVG paths found for ${iconName}`);
const pathXml = paths.map((pathData) => [
' <path',
' android:fillColor="@android:color/transparent"',
' android:strokeColor="@android:color/white"',
' android:strokeWidth="2"',
' android:strokeLineCap="round"',
' android:strokeLineJoin="round"',
` android:pathData="${pathData}" />`,
].join('\n')).join('\n');
const output = [
'<?xml version="1.0" encoding="utf-8"?>',
`<!-- Generated from @tabler/icons ${packageJson.version}: icons/outline/${iconName}.svg -->`,
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
' android:width="24dp"',
' android:height="24dp"',
' android:viewportWidth="24"',
' android:viewportHeight="24">',
pathXml,
'</vector>',
'',
].join('\n');
const outputPath = path.join(drawableRoot, outputName);
if (checkOnly) {
const current = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '';
if (current.replaceAll('\r\n', '\n') !== output) {
console.error(`${outputName} is not synchronized with @tabler/icons`);
mismatches += 1;
}
} else {
fs.writeFileSync(outputPath, output, 'utf8');
}
}
if (mismatches > 0) process.exitCode = 1;
else console.log(`${checkOnly ? 'Verified' : 'Generated'} ${icons.size} Android vectors from @tabler/icons ${packageJson.version}`);