From 4d0789ecd28e7b587555560650641250dffed454 Mon Sep 17 00:00:00 2001
From: Mahdi Karzari <76250959+QM4RS@users.noreply.github.com>
Date: Mon, 27 Jul 2026 16:03:29 +0330
Subject: [PATCH 1/2] Add downloadable gadgets and liquid glass launcher
---
.../DownloadedGadgetRuntime.java | 169 ++++
.../instrumentation/FridaGadgetLoader.java | 3 +-
.../InstrumentationSettings.java | 53 ++
.../LocalScriptGadgetRuntime.java | 12 +-
README.md | 19 +-
THIRD_PARTY_NOTICES.md | 27 +
app/build.gradle | 55 +-
app/src/main/AndroidManifest.xml | 1 +
.../java/com/qm4rs/fridabox/ApkInspector.java | 36 +-
.../com/qm4rs/fridabox/FridaBoxActivity.kt | 896 +++++++++++++++---
.../java/com/qm4rs/fridabox/GadgetManager.kt | 382 ++++++++
.../arm64-v8a/libfrida-gadget.config.so | 11 -
.../main/jniLibs/arm64-v8a/libfrida-gadget.so | Bin 25212656 -> 0 bytes
.../main/res/drawable/bg_fridabox_hero.xml | 6 -
.../main/res/drawable/bg_glass_navigation.xml | 10 +
app/src/main/res/drawable/bg_glass_popup.xml | 10 +
.../main/res/drawable/bg_glass_toolbar.xml | 10 +
.../main/res/drawable/bg_launcher_cell.xml | 17 +
.../res/drawable/bg_launcher_cell_empty.xml | 6 +
.../main/res/drawable/bg_liquid_glass_app.xml | 36 +
app/src/main/res/drawable/ic_fb_add.xml | 4 -
app/src/main/res/drawable/ic_fb_apps.xml | 43 +
app/src/main/res/drawable/ic_fb_clear.xml | 36 +
app/src/main/res/drawable/ic_fb_close.xml | 22 +
app/src/main/res/drawable/ic_fb_details.xml | 29 +
app/src/main/res/drawable/ic_fb_file.xml | 22 +
app/src/main/res/drawable/ic_fb_gadget.xml | 43 +
app/src/main/res/drawable/ic_fb_import.xml | 22 +
app/src/main/res/drawable/ic_fb_remove.xml | 43 +
app/src/main/res/drawable/ic_fb_runtime.xml | 15 +-
app/src/main/res/drawable/ic_fb_settings.xml | 22 +-
app/src/main/res/drawable/ic_fb_workspace.xml | 36 +-
app/src/main/res/layout/activity_fridabox.xml | 51 +-
app/src/main/res/menu/fridabox_navigation.xml | 1 +
app/src/main/res/values/fridabox_colors.xml | 16 +-
app/src/main/res/values/fridabox_strings.xml | 38 +-
app/src/main/res/values/fridabox_styles.xml | 6 +-
.../com/qm4rs/fridabox/ApkInspectorTest.java | 9 +
.../com/qm4rs/fridabox/GadgetManagerTest.kt | 56 ++
docs/BUILDING.md | 24 +-
docs/TEST_RESULTS.md | 22 +-
docs/USAGE.md | 32 +-
package-lock.json | 12 +
package.json | 5 +
tools/fetch_frida_gadget.py | 85 --
tools/frida-gadget-17.16.0.sha256 | 1 -
tools/generate_tabler_android_icons.mjs | 69 ++
47 files changed, 2156 insertions(+), 367 deletions(-)
create mode 100644 Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/DownloadedGadgetRuntime.java
create mode 100644 app/src/main/java/com/qm4rs/fridabox/GadgetManager.kt
delete mode 100644 app/src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so
delete mode 100644 app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so
delete mode 100644 app/src/main/res/drawable/bg_fridabox_hero.xml
create mode 100644 app/src/main/res/drawable/bg_glass_navigation.xml
create mode 100644 app/src/main/res/drawable/bg_glass_popup.xml
create mode 100644 app/src/main/res/drawable/bg_glass_toolbar.xml
create mode 100644 app/src/main/res/drawable/bg_launcher_cell.xml
create mode 100644 app/src/main/res/drawable/bg_launcher_cell_empty.xml
create mode 100644 app/src/main/res/drawable/bg_liquid_glass_app.xml
delete mode 100644 app/src/main/res/drawable/ic_fb_add.xml
create mode 100644 app/src/main/res/drawable/ic_fb_apps.xml
create mode 100644 app/src/main/res/drawable/ic_fb_clear.xml
create mode 100644 app/src/main/res/drawable/ic_fb_close.xml
create mode 100644 app/src/main/res/drawable/ic_fb_details.xml
create mode 100644 app/src/main/res/drawable/ic_fb_file.xml
create mode 100644 app/src/main/res/drawable/ic_fb_gadget.xml
create mode 100644 app/src/main/res/drawable/ic_fb_import.xml
create mode 100644 app/src/main/res/drawable/ic_fb_remove.xml
create mode 100644 app/src/test/java/com/qm4rs/fridabox/GadgetManagerTest.kt
delete mode 100644 tools/fetch_frida_gadget.py
delete mode 100644 tools/frida-gadget-17.16.0.sha256
create mode 100644 tools/generate_tabler_android_icons.mjs
diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/DownloadedGadgetRuntime.java b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/DownloadedGadgetRuntime.java
new file mode 100644
index 0000000..a5010b7
--- /dev/null
+++ b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/DownloadedGadgetRuntime.java
@@ -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());
+ }
+}
diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/FridaGadgetLoader.java b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/FridaGadgetLoader.java
index 4c3d3aa..fcc30a4 100644
--- a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/FridaGadgetLoader.java
+++ b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/FridaGadgetLoader.java
@@ -52,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();
diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/InstrumentationSettings.java b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/InstrumentationSettings.java
index 19f9f57..d080e0f 100644
--- a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/InstrumentationSettings.java
+++ b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/InstrumentationSettings.java
@@ -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;
}
diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/LocalScriptGadgetRuntime.java b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/LocalScriptGadgetRuntime.java
index 68d7224..5e23222 100644
--- a/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/LocalScriptGadgetRuntime.java
+++ b/Bcore/src/main/java/top/niunaijun/blackbox/instrumentation/LocalScriptGadgetRuntime.java
@@ -46,18 +46,10 @@ final class LocalScriptGadgetRuntime {
throw new IOException("Unable to secure the instrumented JavaScript agent");
}
- File source = new File(context.getApplicationInfo().nativeLibraryDir, "libfrida-gadget.so");
- if (!source.isFile()) throw new IOException("Packaged Frida Gadget is missing");
+ 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, instrumentedAgent.getAbsolutePath()));
diff --git a/README.md b/README.md
index 763156e..ae505f4 100644
--- a/README.md
+++ b/README.md
@@ -182,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
@@ -200,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.
@@ -246,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.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 2893e82..b35be7e 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -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
diff --git a/app/build.gradle b/app/build.gradle
index 35c6e14..45b0369 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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,15 @@ 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 entries = zipTree(apk).files.findAll {
+ it.name.toLowerCase().contains("gadget") && it.name.toLowerCase().endsWith(".so")
+ }
+ if (!entries.isEmpty()) {
+ throw new GradleException("APK must not bundle Frida Gadget: ${entries*.name}")
}
}
}
@@ -145,7 +119,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 +127,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 +151,8 @@ tasks.register("verifyDemoGuestUnmodified") {
}
tasks.named("check").configure {
- dependsOn("verifyDebugApkFridaPackaging", "verifyInstrumentationOrdering", "verifyDemoGuestUnmodified")
+ dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
+ "verifyDemoGuestUnmodified", "verifyTablerAndroidIcons")
}
dependencies {
@@ -176,6 +162,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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0108a1e..54aa5a2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/app/src/main/java/com/qm4rs/fridabox/ApkInspector.java b/app/src/main/java/com/qm4rs/fridabox/ApkInspector.java
index 255e3f2..aab920d 100644
--- a/app/src/main/java/com/qm4rs/fridabox/ApkInspector.java
+++ b/app/src/main/java/com/qm4rs/fridabox/ApkInspector.java
@@ -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,23 +14,36 @@ 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 apks) throws IOException {
Set 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]);
+ }
}
}
}
}
+ if (!inspectedAny) throw new IOException("No APK files were provided");
boolean supported = !hasNativeLibraries || abis.contains("arm64-v8a");
return new Result(hasNativeLibraries, supported, abis);
}
diff --git a/app/src/main/java/com/qm4rs/fridabox/FridaBoxActivity.kt b/app/src/main/java/com/qm4rs/fridabox/FridaBoxActivity.kt
index b52c5e1..9d93438 100644
--- a/app/src/main/java/com/qm4rs/fridabox/FridaBoxActivity.kt
+++ b/app/src/main/java/com/qm4rs/fridabox/FridaBoxActivity.kt
@@ -1,5 +1,6 @@
package com.qm4rs.fridabox
+import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -8,26 +9,32 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Typeface
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.text.InputType
import android.view.Gravity
-import android.view.Menu
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.FrameLayout
+import android.widget.GridLayout
import android.widget.ImageView
import android.widget.LinearLayout
-import android.widget.PopupMenu
+import android.widget.PopupWindow
import android.widget.Space
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
+import androidx.core.widget.NestedScrollView
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.card.MaterialCardView
@@ -47,12 +54,14 @@ import java.util.concurrent.Executors
/** Product workspace for importing, configuring, and launching FridaBox guests. */
class FridaBoxActivity : AppCompatActivity() {
- private enum class Screen { WORKSPACE, RUNTIME, SETTINGS }
+ private enum class Screen { WORKSPACE, GADGETS, RUNTIME, SETTINGS }
private lateinit var binding: ActivityFridaboxBinding
private val worker = Executors.newSingleThreadExecutor()
private var screen = Screen.WORKSPACE
private var screenGeneration = 0
+ private var catalogRequest = 0
+ private var installedAppsRequest = 0
private var changingNavigation = false
private var pendingScriptPackage: String? = null
@@ -63,6 +72,7 @@ class FridaBoxActivity : AppCompatActivity() {
private val metadata: SharedPreferences by lazy {
getSharedPreferences("fridabox_imports", Context.MODE_PRIVATE)
}
+ private val gadgetManager: GadgetManager by lazy { GadgetManager(applicationContext) }
private val apkPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) importApk(uri)
@@ -79,11 +89,12 @@ class FridaBoxActivity : AppCompatActivity() {
setContentView(binding.root)
pendingScriptPackage = savedInstanceState?.getString("pending_script_package")
- binding.importFab.setOnClickListener { openApkPicker() }
+ binding.importFabIcon.setOnClickListener { showImportActions() }
binding.bottomNavigation.setOnItemSelectedListener { item ->
if (changingNavigation) return@setOnItemSelectedListener true
when (item.itemId) {
R.id.nav_workspace -> showWorkspace()
+ R.id.nav_gadgets -> showGadgets()
R.id.nav_runtime -> showRuntime()
R.id.nav_settings -> showSettings()
else -> return@setOnItemSelectedListener false
@@ -110,51 +121,57 @@ class FridaBoxActivity : AppCompatActivity() {
private fun showWorkspace() {
screen = Screen.WORKSPACE
- val generation = resetScreen(R.id.nav_workspace, showImport = false)
+ val generation = resetScreen(R.id.nav_workspace, showImport = true)
binding.toolbar.title = getString(R.string.fb_brand)
binding.toolbar.subtitle = getString(R.string.fb_brand_tagline)
- binding.content.addView(heroCard())
binding.content.requestFocus()
binding.contentScroll.post {
binding.content.requestFocus()
binding.contentScroll.scrollTo(0, 0)
}
- val titleRow = LinearLayout(this).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER_VERTICAL
- setPadding(0, dp(28), 0, dp(10))
+ val screenWidthDp = resources.configuration.screenWidthDp
+ val columns = when {
+ screenWidthDp < 600 -> 4
+ screenWidthDp < 840 -> 6
+ else -> 7
}
- titleRow.addView(verticalText(
- getString(R.string.fb_guest_workspace),
- getString(R.string.fb_guest_workspace_hint)
- ), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
- val count = badge("…", color(R.color.fb_surface_high), color(R.color.fb_primary))
- titleRow.addView(count)
- binding.content.addView(titleRow)
- binding.content.addView(primaryButton(getString(R.string.fb_import_apk)) {
- openApkPicker()
- }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)).apply {
- bottomMargin = dp(14)
- })
-
- val guestList = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
- binding.content.addView(guestList)
+ val cellSizeDp = ((screenWidthDp - 36) / columns).coerceIn(78, 112)
+ val minimumRows = ((resources.configuration.screenHeightDp - 276) / cellSizeDp)
+ .coerceAtLeast(4)
+ val launcher = GridLayout(this).apply {
+ columnCount = columns
+ alignmentMode = GridLayout.ALIGN_BOUNDS
+ useDefaultMargins = false
+ setPadding(0, 0, 0, 0)
+ }
+ binding.content.addView(launcher, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ ))
setLoading(true)
worker.execute {
val result = runCatching {
BlackBoxCore.get().getInstalledPackages(PackageManager.GET_META_DATA, 0)
- .sortedBy { it.packageName }
+ .sortedBy { info ->
+ runCatching {
+ info.applicationInfo?.loadLabel(BlackBoxCore.getPackageManager())?.toString()
+ }.getOrNull().orEmpty().ifBlank { info.packageName }.lowercase(Locale.ROOT)
+ }
}
runOnUiThread {
if (generation != screenGeneration || isFinishing) return@runOnUiThread
setLoading(false)
result.onSuccess { packages ->
- count.text = packages.size.toString()
- if (packages.isEmpty()) guestList.addView(emptyWorkspace())
- else packages.forEach { guestList.addView(appCard(it)) }
+ packages.forEach { launcher.addView(appIcon(it, cellSizeDp)) }
+ val occupiedSlots = packages.size
+ val completedRows = (occupiedSlots + columns - 1) / columns
+ val totalSlots = maxOf(minimumRows, completedRows) * columns
+ repeat(totalSlots - occupiedSlots) {
+ launcher.addView(emptyLauncherCell(cellSizeDp))
+ }
}.onFailure { error ->
- guestList.addView(messageCard(
+ binding.content.addView(messageCard(
"Workspace unavailable",
error.message ?: "Unable to read virtual applications",
R.color.fb_error
@@ -164,54 +181,77 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
- private fun heroCard(): View {
- val body = LinearLayout(this).apply {
- orientation = LinearLayout.VERTICAL
- setPadding(dp(22), dp(22), dp(22), dp(22))
- background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_fridabox_hero)
- }
- body.addView(labelText(getString(R.string.fb_hero_eyebrow), R.color.fb_primary, 11f, true))
- body.addView(labelText(getString(R.string.fb_hero_title), R.color.fb_text_primary, 29f, true).apply {
- setPadding(0, dp(8), 0, 0)
- })
- body.addView(labelText(getString(R.string.fb_hero_body), R.color.fb_text_secondary, 15f, false).apply {
- setPadding(0, dp(10), 0, 0)
- setLineSpacing(0f, 1.15f)
- })
- val signals = LinearLayout(this).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.START
- setPadding(0, dp(18), 0, 0)
- }
- signals.addView(badge("ARM64", color(R.color.fb_surface_tint), color(R.color.fb_primary)))
- signals.addView(space(dp(8), 1))
- signals.addView(badge("v${BuildConfig.VERSION_NAME}", color(R.color.fb_surface_tint), color(R.color.fb_text_primary)))
- body.addView(signals)
- return body
- }
-
- private fun emptyWorkspace(): View {
- return surfaceCard().apply {
- addView(LinearLayout(this@FridaBoxActivity).apply {
- orientation = LinearLayout.VERTICAL
- gravity = Gravity.CENTER_HORIZONTAL
- setPadding(dp(24), dp(36), dp(24), dp(36))
- addView(ImageView(this@FridaBoxActivity).apply {
- setImageResource(R.drawable.logo)
- }, LinearLayout.LayoutParams(dp(52), dp(52)))
- addView(labelText(getString(R.string.fb_no_guests), R.color.fb_text_primary, 19f, true).apply {
- setPadding(0, dp(16), 0, 0)
- })
- addView(labelText(getString(R.string.fb_no_guests_body), R.color.fb_text_secondary, 14f, false).apply {
- gravity = Gravity.CENTER
- setPadding(0, dp(8), 0, dp(18))
- })
- addView(primaryButton(getString(R.string.fb_import_apk)) { openApkPicker() })
- })
+ private fun appIcon(info: PackageInfo, cellSizeDp: Int): View {
+ val packageName = info.packageName
+ val appLabel = runCatching {
+ info.applicationInfo?.loadLabel(BlackBoxCore.getPackageManager())?.toString()
+ }.getOrNull().orEmpty().ifBlank { packageName.substringAfterLast('.') }
+ return FrameLayout(this).apply {
+ contentDescription = appLabel
+ isClickable = true
+ isFocusable = true
+ background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_launcher_cell)
+ setOnClickListener { showAppLaunchDialog(info) }
+ addView(ImageView(this@FridaBoxActivity).apply {
+ runCatching {
+ setImageDrawable(info.applicationInfo?.loadIcon(BlackBoxCore.getPackageManager()))
+ }
+ scaleType = ImageView.ScaleType.FIT_CENTER
+ }, FrameLayout.LayoutParams(
+ dp((cellSizeDp * 0.62f).toInt().coerceIn(48, 64)),
+ dp((cellSizeDp * 0.62f).toInt().coerceIn(48, 64)),
+ Gravity.CENTER
+ ))
+ layoutParams = launcherCellLayoutParams(cellSizeDp)
}
}
- private fun appCard(info: PackageInfo): View {
+ private fun emptyLauncherCell(cellSizeDp: Int): View = View(this).apply {
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_launcher_cell_empty)
+ layoutParams = launcherCellLayoutParams(cellSizeDp)
+ }
+
+ private fun launcherCellLayoutParams(cellSizeDp: Int): GridLayout.LayoutParams {
+ return GridLayout.LayoutParams().apply {
+ width = 0
+ height = dp(cellSizeDp)
+ columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
+ setMargins(dp(4), dp(4), dp(4), dp(4))
+ }
+ }
+
+ private fun showAppLaunchDialog(info: PackageInfo) {
+ val dialog = Dialog(this)
+ val root = FrameLayout(this).apply {
+ setPadding(dp(12), dp(24), dp(12), dp(12))
+ setOnClickListener { dialog.dismiss() }
+ }
+ val panel = appCard(info, dialog).apply { isClickable = true }
+ root.addView(panel, FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.BOTTOM
+ ))
+ dialog.setContentView(root)
+ dialog.setCanceledOnTouchOutside(true)
+ dialog.window?.apply {
+ setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
+ addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ attributes = attributes.apply { dimAmount = 0.42f }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
+ attributes = attributes.apply { blurBehindRadius = dp(36) }
+ }
+ }
+ dialog.show()
+ dialog.window?.setLayout(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+
+ private fun appCard(info: PackageInfo, dialog: Dialog): View {
val packageName = info.packageName
val mode = InstrumentationSettings.getModeForPackage(packageName)
val appLabel = runCatching {
@@ -219,6 +259,8 @@ class FridaBoxActivity : AppCompatActivity() {
}.getOrNull().orEmpty().ifBlank { packageName.substringAfterLast('.') }
val card = surfaceCard().apply {
+ radius = dp(28).toFloat()
+ setCardBackgroundColor(color(R.color.fb_glass_surface_strong))
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
@@ -236,7 +278,7 @@ class FridaBoxActivity : AppCompatActivity() {
}
val icon = ImageView(this).apply {
runCatching { setImageDrawable(info.applicationInfo?.loadIcon(BlackBoxCore.getPackageManager())) }
- background = rounded(color(R.color.fb_surface_tint), dp(16))
+ background = rounded(color(R.color.fb_glass_surface_soft), dp(16))
setPadding(dp(8), dp(8), dp(8), dp(8))
}
heading.addView(icon, LinearLayout.LayoutParams(dp(58), dp(58)))
@@ -290,6 +332,7 @@ class FridaBoxActivity : AppCompatActivity() {
setPadding(0, dp(16), 0, 0)
}
actions.addView(primaryButton(getString(R.string.fb_launch)) {
+ dialog.dismiss()
launchConfigured(packageName)
}, LinearLayout.LayoutParams(0, dp(50), 1f))
actions.addView(space(dp(10), 1))
@@ -321,7 +364,7 @@ class FridaBoxActivity : AppCompatActivity() {
val panel = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(13), dp(12), dp(13), dp(12))
- background = rounded(color(R.color.fb_surface_tint), dp(14))
+ background = rounded(color(R.color.fb_glass_surface_soft), dp(16))
}
panel.addView(labelText(title, R.color.fb_text_primary, 14f, true))
panel.addView(labelText(description, R.color.fb_text_secondary, 12.5f, false).apply {
@@ -355,6 +398,13 @@ class FridaBoxActivity : AppCompatActivity() {
private fun launchConfigured(packageName: String) {
val mode = InstrumentationSettings.getModeForPackage(packageName)
+ if (mode != InstrumentationSettings.MODE_CLEAN
+ && settings.getBoolean(InstrumentationSettings.KEY_ENABLED, true)
+ && gadgetManager.selected() == null) {
+ notify(getString(R.string.fb_gadget_required))
+ showGadgets()
+ return
+ }
if (mode == InstrumentationSettings.MODE_LOCAL_SCRIPT) {
val path = InstrumentationSettings.getScriptPathForPackage(packageName)
if (path.isNullOrBlank() || !File(path).isFile) {
@@ -365,12 +415,11 @@ class FridaBoxActivity : AppCompatActivity() {
return
}
if (mode == InstrumentationSettings.MODE_COMPUTER) {
- MaterialAlertDialogBuilder(this)
+ showGlassAlert(MaterialAlertDialogBuilder(this)
.setTitle(R.string.fb_computer_launch_title)
.setMessage(R.string.fb_computer_launch_body)
.setPositiveButton(R.string.fb_launch) { _, _ -> launch(packageName, mode) }
- .setNegativeButton(R.string.fb_cancel, null)
- .show()
+ .setNegativeButton(R.string.fb_cancel, null))
return
}
launch(packageName, mode)
@@ -406,7 +455,7 @@ class FridaBoxActivity : AppCompatActivity() {
}
private fun chooseAgent(packageName: String) {
- MaterialAlertDialogBuilder(this)
+ showGlassAlert(MaterialAlertDialogBuilder(this)
.setTitle(R.string.fb_choose_trusted_title)
.setMessage(R.string.fb_choose_trusted_body)
.setPositiveButton(R.string.fb_choose) { _, _ ->
@@ -418,8 +467,7 @@ class FridaBoxActivity : AppCompatActivity() {
"application/octet-stream"
))
}
- .setNegativeButton(R.string.fb_cancel, null)
- .show()
+ .setNegativeButton(R.string.fb_cancel, null))
}
private fun importAgent(packageName: String, uri: Uri) {
@@ -482,22 +530,41 @@ class FridaBoxActivity : AppCompatActivity() {
}
private fun showAppMenu(anchor: View, info: PackageInfo, appLabel: String) {
- PopupMenu(this, anchor).apply {
- menu.add(Menu.NONE, MENU_DETAILS, 0, R.string.fb_details)
- menu.add(Menu.NONE, MENU_RUNTIME, 1, R.string.fb_runtime_details)
- menu.add(Menu.NONE, MENU_CLEAR, 2, R.string.fb_clear_data)
- menu.add(Menu.NONE, MENU_REMOVE, 3, R.string.fb_remove_guest)
- setOnMenuItemClickListener { item ->
- when (item.itemId) {
- MENU_DETAILS -> showAppDetails(info, appLabel)
- MENU_RUNTIME -> showRuntime(info.packageName)
- MENU_CLEAR -> clearApp(info.packageName)
- MENU_REMOVE -> removeApp(info.packageName, appLabel)
- }
- true
- }
- show()
+ val popup = PopupWindow(this).apply {
+ width = dp(250)
+ height = ViewGroup.LayoutParams.WRAP_CONTENT
+ isFocusable = true
+ isOutsideTouchable = true
+ setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
+ elevation = dp(14).toFloat()
}
+ val panel = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(10), dp(10), dp(10), dp(2))
+ background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_glass_popup)
+ }
+ panel.addView(menuActionButton(getString(R.string.fb_details), R.drawable.ic_fb_details) {
+ popup.dismiss()
+ showAppDetails(info, appLabel)
+ })
+ panel.addView(menuActionButton(getString(R.string.fb_runtime_details), R.drawable.ic_fb_runtime) {
+ popup.dismiss()
+ showRuntime(info.packageName)
+ })
+ panel.addView(menuActionButton(getString(R.string.fb_clear_data), R.drawable.ic_fb_clear) {
+ popup.dismiss()
+ clearApp(info.packageName)
+ })
+ panel.addView(menuActionButton(getString(R.string.fb_remove_guest), R.drawable.ic_fb_remove) {
+ popup.dismiss()
+ removeApp(info.packageName, appLabel)
+ })
+ popup.contentView = panel
+ popup.showAtLocation(anchor, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, dp(28))
+ }
+
+ private fun menuActionButton(label: String, iconResource: Int, action: () -> Unit): MaterialButton {
+ return importActionButton(label, iconResource, action)
}
private fun showAppDetails(info: PackageInfo, appLabel: String) {
@@ -510,11 +577,10 @@ class FridaBoxActivity : AppCompatActivity() {
append("\n\nSource\n").append(metadata.getString("$packageName.source", "Unknown"))
append("\n\nAPK SHA-256\n").append(metadata.getString("$packageName.sha256", "Unknown"))
}
- MaterialAlertDialogBuilder(this)
+ showGlassAlert(MaterialAlertDialogBuilder(this)
.setTitle(R.string.fb_details)
.setMessage(details)
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ .setPositiveButton(android.R.string.ok, null))
}
private fun clearApp(packageName: String) {
@@ -533,7 +599,7 @@ class FridaBoxActivity : AppCompatActivity() {
}
private fun removeApp(packageName: String, appLabel: String) {
- MaterialAlertDialogBuilder(this)
+ showGlassAlert(MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.fb_remove_title))
.setMessage("$appLabel\n\n${getString(R.string.fb_remove_body)}")
.setPositiveButton(R.string.fb_remove) { _, _ ->
@@ -557,14 +623,273 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
}
- .setNegativeButton(R.string.fb_cancel, null)
- .show()
+ .setNegativeButton(R.string.fb_cancel, null))
+ }
+
+ private fun showGlassAlert(builder: MaterialAlertDialogBuilder): AlertDialog {
+ return builder.create().also { dialog ->
+ dialog.setOnShowListener {
+ dialog.window?.apply {
+ setBackgroundDrawable(ContextCompat.getDrawable(
+ this@FridaBoxActivity,
+ R.drawable.bg_glass_popup
+ ))
+ addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ attributes = attributes.apply { dimAmount = 0.42f }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
+ attributes = attributes.apply { blurBehindRadius = dp(36) }
+ }
+ }
+ }
+ dialog.show()
+ }
}
private fun openApkPicker() {
apkPicker.launch(arrayOf("application/vnd.android.package-archive", "application/octet-stream"))
}
+ private fun showImportActions() {
+ val popup = PopupWindow(this).apply {
+ width = dp(232)
+ height = ViewGroup.LayoutParams.WRAP_CONTENT
+ isFocusable = true
+ isOutsideTouchable = true
+ setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
+ elevation = dp(12).toFloat()
+ }
+ val panel = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(10), dp(10), dp(10), dp(2))
+ background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_glass_popup)
+ }
+ panel.addView(importActionButton(
+ getString(R.string.fb_import_from_apps),
+ R.drawable.ic_fb_apps
+ ) {
+ popup.dismiss()
+ showInstalledAppsBrowser()
+ })
+ panel.addView(importActionButton(
+ getString(R.string.fb_import_from_file),
+ R.drawable.ic_fb_file
+ ) {
+ popup.dismiss()
+ openApkPicker()
+ })
+ popup.contentView = panel
+ popup.showAtLocation(binding.root, Gravity.BOTTOM or Gravity.END, dp(18), dp(156))
+ }
+
+ private fun importActionButton(label: String, iconResource: Int, action: () -> Unit): MaterialButton {
+ return outlineButton(label) { action() }.apply {
+ icon = ContextCompat.getDrawable(this@FridaBoxActivity, iconResource)
+ iconTint = ColorStateList.valueOf(color(R.color.fb_text_primary))
+ iconPadding = dp(12)
+ iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
+ gravity = Gravity.START or Gravity.CENTER_VERTICAL
+ layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply {
+ bottomMargin = dp(8)
+ }
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun showInstalledAppsBrowser() {
+ val dialog = Dialog(this)
+ val root = FrameLayout(this).apply {
+ setPadding(dp(12), dp(32), dp(12), dp(12))
+ setOnClickListener { dialog.dismiss() }
+ }
+ val panel = surfaceCard().apply {
+ radius = dp(28).toFloat()
+ setCardBackgroundColor(color(R.color.fb_glass_surface_strong))
+ isClickable = true
+ }
+ val body = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(18), dp(18), dp(18), dp(10))
+ }
+ panel.addView(body)
+ body.addView(LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ addView(labelText(
+ getString(R.string.fb_installed_apps),
+ R.color.fb_text_primary,
+ 19f,
+ true
+ ), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
+ addView(iconButton(R.drawable.ic_fb_close, getString(R.string.fb_close)) { dialog.dismiss() },
+ LinearLayout.LayoutParams(dp(40), dp(40)))
+ })
+
+ val list = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, dp(10), 0, 0)
+ }
+ val scroll = NestedScrollView(this).apply {
+ isFillViewport = false
+ overScrollMode = View.OVER_SCROLL_NEVER
+ addView(list, ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ ))
+ }
+ body.addView(scroll, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 0,
+ 1f
+ ))
+ val footer = labelText(
+ getString(R.string.fb_loading_apps),
+ R.color.fb_text_secondary,
+ 12f,
+ false
+ ).apply {
+ gravity = Gravity.CENTER
+ setPadding(0, dp(14), 0, dp(8))
+ }
+ body.addView(footer)
+
+ root.addView(panel, FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ (resources.displayMetrics.heightPixels * 0.82f).toInt(),
+ Gravity.BOTTOM
+ ))
+ dialog.setContentView(root)
+ dialog.setCanceledOnTouchOutside(true)
+ dialog.window?.apply {
+ setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
+ addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ attributes = attributes.apply { dimAmount = 0.42f }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
+ attributes = attributes.apply { blurBehindRadius = dp(36) }
+ }
+ }
+ dialog.show()
+ dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+
+ val request = ++installedAppsRequest
+ worker.execute {
+ val result = runCatching {
+ val imported = BlackBoxCore.get()
+ .getInstalledPackages(0, 0)
+ .mapTo(HashSet()) { it.packageName }
+ val packages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
+ .asSequence()
+ .filter { it.packageName != packageName }
+ .filter { it.applicationInfo?.enabled == true }
+ .filter { packageManager.getLaunchIntentForPackage(it.packageName) != null }
+ .filter { info ->
+ info.applicationInfo?.sourceDir?.let { File(it).isFile } == true
+ }
+ .sortedBy { info ->
+ info.applicationInfo?.loadLabel(packageManager)?.toString()
+ .orEmpty().ifBlank { info.packageName }.lowercase(Locale.ROOT)
+ }
+ .toList()
+ packages to imported
+ }
+ runOnUiThread {
+ if (request != installedAppsRequest || !dialog.isShowing || isFinishing) {
+ return@runOnUiThread
+ }
+ result.onSuccess { (packages, imported) ->
+ footer.isVisible = packages.isEmpty()
+ if (packages.isEmpty()) footer.text = getString(R.string.fb_no_importable_apps)
+ packages.forEach { info ->
+ list.addView(installedAppRow(info, info.packageName in imported, dialog))
+ }
+ }.onFailure { error ->
+ footer.text = error.message ?: getString(R.string.fb_no_importable_apps)
+ }
+ }
+ }
+ }
+
+ private fun installedAppRow(info: PackageInfo, imported: Boolean, dialog: Dialog): View {
+ val appInfo = info.applicationInfo
+ val label = runCatching { appInfo?.loadLabel(packageManager)?.toString() }
+ .getOrNull().orEmpty().ifBlank { info.packageName }
+ return LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ setPadding(0, dp(9), 0, dp(9))
+ addView(ImageView(this@FridaBoxActivity).apply {
+ runCatching { setImageDrawable(appInfo?.loadIcon(packageManager)) }
+ scaleType = ImageView.ScaleType.FIT_CENTER
+ }, LinearLayout.LayoutParams(dp(46), dp(46)))
+ addView(LinearLayout(this@FridaBoxActivity).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(12), 0, dp(8), 0)
+ addView(labelText(label, R.color.fb_text_primary, 14.5f, true).apply {
+ maxLines = 1
+ ellipsize = android.text.TextUtils.TruncateAt.END
+ })
+ addView(labelText(info.packageName, R.color.fb_text_secondary, 10.5f, false).apply {
+ maxLines = 1
+ ellipsize = android.text.TextUtils.TruncateAt.END
+ })
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
+ addView(outlineButton(getString(
+ if (imported) R.string.fb_app_imported else R.string.fb_import_app
+ )) {
+ dialog.dismiss()
+ importInstalledApp(info.packageName)
+ }.apply {
+ isEnabled = !imported
+ }, LinearLayout.LayoutParams(dp(92), dp(40)))
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun importInstalledApp(packageName: String) {
+ setLoading(true)
+ worker.execute {
+ val result = runCatching {
+ require(packageName != this.packageName) { "FridaBox cannot import itself" }
+ val info = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)
+ val appInfo = requireNotNull(info.applicationInfo) { "Application metadata is unavailable" }
+ val apkFiles = buildList {
+ appInfo.sourceDir?.let { add(File(it)) }
+ appInfo.splitSourceDirs?.forEach { add(File(it)) }
+ }.distinctBy { it.canonicalPath }
+ require(apkFiles.isNotEmpty() && apkFiles.all { it.isFile }) {
+ "Installed APK files are unavailable"
+ }
+ val abi = ApkInspector.inspect(apkFiles)
+ if (!abi.supported) error("Unsupported native ABI: ${abi.description()}")
+ val originalHash = installedPackageSha256(apkFiles)
+ val install = BlackBoxCore.get().installPackageAsUser(packageName, 0)
+ if (!install.success) error(install.msg ?: "Virtual installation failed")
+ if (installedPackageSha256(apkFiles) != originalHash) {
+ error("Installed package changed during import")
+ }
+ metadata.edit()
+ .putString("$packageName.sha256", originalHash)
+ .putString("$packageName.source", appInfo.sourceDir)
+ .putString("$packageName.abi", abi.description())
+ .putString("$packageName.version", info.versionName)
+ .putInt("$packageName.targetSdk", appInfo.targetSdkVersion)
+ .putInt("$packageName.apkCount", apkFiles.size)
+ .putBoolean("$packageName.fromInstalledApp", true)
+ .apply()
+ InstrumentationSettings.setModeForPackage(packageName, InstrumentationSettings.MODE_COMPUTER)
+ appInfo.loadLabel(packageManager).toString().ifBlank { packageName }
+ }
+ runOnUiThread {
+ setLoading(false)
+ result.onSuccess {
+ notify("$it imported from installed apps")
+ showWorkspace()
+ }.onFailure { notify("App import failed: ${it.message}") }
+ }
+ }
+ }
+
private fun importApk(uri: Uri) {
val name = displayName(uri, "selected.apk")
val lowerName = name.lowercase(Locale.ROOT)
@@ -629,6 +954,283 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
+ private fun showGadgets() {
+ screen = Screen.GADGETS
+ resetScreen(R.id.nav_gadgets, showImport = false)
+ binding.toolbar.title = getString(R.string.fb_gadgets_title)
+
+ val abi = gadgetManager.detectedAbi()
+ if (abi == null) {
+ binding.toolbar.subtitle = null
+ binding.content.addView(messageCard(
+ getString(R.string.fb_gadget_unsupported_title),
+ Build.SUPPORTED_ABIS.joinToString(),
+ R.color.fb_error
+ ))
+ return
+ }
+ binding.toolbar.subtitle = abi.androidName
+
+ val selected = gadgetManager.selected()
+ binding.content.addView(LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, dp(8), 0, dp(20))
+ addView(labelText(getString(R.string.fb_active_gadget), R.color.fb_text_secondary, 12f, false))
+ addView(labelText(
+ selected?.version ?: getString(R.string.fb_no_active_gadget_short),
+ R.color.fb_text_primary,
+ 22f,
+ true
+ ).apply { setPadding(0, dp(3), 0, 0) })
+ if (selected != null) {
+ addView(labelText(
+ "${selected.source.title} / ${selected.abi.androidName}",
+ R.color.fb_text_secondary,
+ 12.5f,
+ false
+ ).apply { setPadding(0, dp(3), 0, 0) })
+ }
+ })
+
+ binding.content.addView(primaryButton(getString(R.string.fb_browse_gadgets)) {
+ showGadgetBrowser(abi)
+ }.apply {
+ layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(52)).apply {
+ bottomMargin = dp(24)
+ }
+ })
+
+ val installed = gadgetManager.installed()
+ if (installed.isNotEmpty()) {
+ binding.content.addView(labelText(
+ getString(R.string.fb_installed_gadgets), R.color.fb_text_secondary, 12f, false
+ ).apply { setPadding(0, 0, 0, dp(6)) })
+ installed.forEach { gadget ->
+ binding.content.addView(installedGadgetRow(gadget, selected?.identity == gadget.identity))
+ }
+ }
+ }
+
+ private fun installedGadgetRow(gadget: InstalledGadget, active: Boolean): View {
+ return gadgetRow(
+ gadget.version,
+ "${gadget.abi.androidName} / ${humanSize(gadget.size)}",
+ if (active) getString(R.string.fb_gadget_active) else getString(R.string.fb_use_gadget),
+ active
+ ) {
+ runCatching { gadgetManager.select(gadget) }
+ .onSuccess {
+ notify(getString(R.string.fb_gadget_selected, gadget.version))
+ showGadgets()
+ }
+ .onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
+ }
+ }
+
+ private fun releaseRow(release: GadgetRelease, dialog: Dialog): View {
+ val installed = gadgetManager.findInstalled(release)
+ val activePath = InstrumentationSettings.getSelectedGadgetPath()
+ val active = installed != null && runCatching {
+ installed.file.canonicalPath == File(activePath.orEmpty()).canonicalPath
+ }.getOrDefault(false)
+ val details = listOfNotNull(
+ release.publishedAt.takeIf { it.isNotBlank() }?.take(10),
+ release.compressedSize.takeIf { it > 0 }?.let(::humanSize)
+ ).joinToString(" / ")
+ return gadgetRow(
+ release.version,
+ details,
+ when {
+ active -> getString(R.string.fb_gadget_active)
+ installed != null -> getString(R.string.fb_use_gadget)
+ else -> getString(R.string.fb_download)
+ },
+ active
+ ) {
+ when {
+ installed != null -> runCatching { gadgetManager.select(installed) }
+ .onSuccess {
+ dialog.dismiss()
+ showGadgets()
+ }
+ .onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
+ else -> {
+ dialog.dismiss()
+ downloadGadget(release)
+ }
+ }
+ }
+ }
+
+ private fun gadgetRow(
+ version: String,
+ details: String,
+ actionLabel: String,
+ actionDisabled: Boolean,
+ action: () -> Unit
+ ): View = LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ setPadding(0, dp(10), 0, dp(10))
+ addView(LinearLayout(this@FridaBoxActivity).apply {
+ orientation = LinearLayout.VERTICAL
+ addView(labelText(version, R.color.fb_text_primary, 16f, true))
+ if (details.isNotBlank()) {
+ addView(labelText(details, R.color.fb_text_secondary, 11.5f, false).apply {
+ setPadding(0, dp(2), 0, 0)
+ })
+ }
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
+ addView(outlineButton(actionLabel) { action() }.apply {
+ isEnabled = !actionDisabled
+ }, LinearLayout.LayoutParams(dp(106), dp(40)).apply { marginStart = dp(12) })
+ }
+
+ private fun showGadgetBrowser(abi: GadgetAbi) {
+ val dialog = Dialog(this)
+ val root = FrameLayout(this).apply {
+ setPadding(dp(12), dp(32), dp(12), dp(12))
+ setOnClickListener { dialog.dismiss() }
+ }
+ val panel = surfaceCard().apply {
+ radius = dp(28).toFloat()
+ setCardBackgroundColor(color(R.color.fb_glass_surface_strong))
+ isClickable = true
+ }
+ val body = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(18), dp(18), dp(18), dp(10))
+ }
+ panel.addView(body)
+
+ body.addView(LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ addView(verticalText(
+ getString(R.string.fb_browse_gadgets),
+ abi.androidName
+ ), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
+ addView(iconButton(R.drawable.ic_fb_close, getString(R.string.fb_close)) { dialog.dismiss() },
+ LinearLayout.LayoutParams(dp(40), dp(40)))
+ })
+
+ val list = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, dp(10), 0, 0)
+ }
+ val scroll = NestedScrollView(this).apply {
+ isFillViewport = false
+ overScrollMode = View.OVER_SCROLL_NEVER
+ addView(list, ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ ))
+ }
+ body.addView(scroll, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 0,
+ 1f
+ ))
+ val footer = labelText("", R.color.fb_text_secondary, 12f, false).apply {
+ gravity = Gravity.CENTER
+ setPadding(0, dp(10), 0, dp(4))
+ isVisible = false
+ }
+ body.addView(footer)
+
+ root.addView(panel, FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ (resources.displayMetrics.heightPixels * 0.82f).toInt(),
+ Gravity.BOTTOM
+ ))
+ dialog.setContentView(root)
+ dialog.setCanceledOnTouchOutside(true)
+ dialog.window?.apply {
+ setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
+ addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ attributes = attributes.apply { dimAmount = 0.42f }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
+ attributes = attributes.apply { blurBehindRadius = dp(36) }
+ }
+ }
+ dialog.show()
+ dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+
+ val request = ++catalogRequest
+ val identities = HashSet()
+ var page = 1
+ var loading = false
+ var hasMore = true
+ val loader = object : Runnable {
+ override fun run() {
+ if (loading || !hasMore || !dialog.isShowing) return
+ loading = true
+ footer.text = getString(R.string.fb_loading_versions)
+ footer.isVisible = true
+ footer.setOnClickListener(null)
+ worker.execute {
+ val result = runCatching {
+ gadgetManager.loadCatalog(GadgetSource.OFFICIAL, abi, page)
+ }
+ runOnUiThread {
+ if (request != catalogRequest || !dialog.isShowing || isFinishing) {
+ return@runOnUiThread
+ }
+ loading = false
+ result.onSuccess { catalog ->
+ catalog.releases.forEach { release ->
+ val identity = "${release.source.id}:${release.version}:${release.abi.androidName}"
+ if (identities.add(identity)) list.addView(releaseRow(release, dialog))
+ }
+ page += 1
+ hasMore = catalog.hasMore
+ footer.text = when {
+ !hasMore -> getString(R.string.fb_all_versions_loaded)
+ catalog.fromCache -> getString(R.string.fb_cached_versions)
+ else -> ""
+ }
+ footer.isVisible = !hasMore || catalog.fromCache
+ if (hasMore && (catalog.releases.isEmpty() || !scroll.canScrollVertically(1))) {
+ scroll.post(this)
+ }
+ }.onFailure { error ->
+ footer.text = error.message ?: getString(R.string.fb_catalog_failed_body)
+ footer.isVisible = true
+ footer.setOnClickListener { run() }
+ }
+ }
+ }
+ }
+ }
+ scroll.setOnScrollChangeListener { view: NestedScrollView, _, scrollY, _, _ ->
+ val child = view.getChildAt(0) ?: return@setOnScrollChangeListener
+ if (child.bottom - (view.height + scrollY) <= dp(120)) loader.run()
+ }
+ loader.run()
+ }
+
+ private fun downloadGadget(release: GadgetRelease) {
+ setLoading(true)
+ notify(getString(R.string.fb_gadget_downloading, release.version))
+ worker.execute {
+ val hadSelection = gadgetManager.selected() != null
+ val result = runCatching {
+ gadgetManager.download(release).also { if (!hadSelection) gadgetManager.select(it) }
+ }
+ runOnUiThread {
+ setLoading(false)
+ result.onSuccess {
+ notify(getString(
+ if (hadSelection) R.string.fb_gadget_downloaded else R.string.fb_gadget_downloaded_selected,
+ release.version
+ ))
+ showGadgets()
+ }.onFailure { notify(getString(R.string.fb_gadget_download_failed, it.message ?: "Unknown error")) }
+ }
+ }
+ }
+
private fun showRuntime(packageHint: String? = null) {
screen = Screen.RUNTIME
resetScreen(R.id.nav_runtime, showImport = false)
@@ -772,7 +1374,8 @@ class FridaBoxActivity : AppCompatActivity() {
return TextInputLayout(this).apply {
hint = label
boxBackgroundMode = TextInputLayout.BOX_BACKGROUND_OUTLINE
- boxStrokeColor = color(R.color.fb_outline)
+ boxBackgroundColor = color(R.color.fb_glass_surface_soft)
+ boxStrokeColor = color(R.color.fb_glass_outline)
defaultHintTextColor = ColorStateList.valueOf(color(R.color.fb_text_secondary))
setPadding(0, dp(12), 0, 0)
addView(input)
@@ -856,9 +1459,9 @@ class FridaBoxActivity : AppCompatActivity() {
private fun surfaceCard(): MaterialCardView = MaterialCardView(this).apply {
radius = resources.getDimension(R.dimen.fb_card_radius)
- cardElevation = 0f
- setCardBackgroundColor(color(R.color.fb_surface))
- strokeColor = color(R.color.fb_outline)
+ cardElevation = dp(8).toFloat()
+ setCardBackgroundColor(color(R.color.fb_glass_surface))
+ strokeColor = color(R.color.fb_glass_outline)
strokeWidth = dp(1)
}
@@ -894,8 +1497,8 @@ class FridaBoxActivity : AppCompatActivity() {
insetTop = 0
insetBottom = 0
setTextColor(checkedColors(color(R.color.fb_black), color(R.color.fb_text_primary)))
- strokeColor = checkedColors(color(R.color.fb_primary), color(R.color.fb_outline))
- backgroundTintList = checkedColors(color(R.color.fb_primary), color(R.color.fb_surface))
+ strokeColor = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_outline))
+ backgroundTintList = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_surface_soft))
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
}
}
@@ -905,11 +1508,14 @@ class FridaBoxActivity : AppCompatActivity() {
text = label
isAllCaps = false
letterSpacing = 0f
- cornerRadius = dp(15)
+ cornerRadius = dp(18)
insetTop = 0
insetBottom = 0
setTextColor(color(R.color.fb_black))
backgroundTintList = ColorStateList.valueOf(color(R.color.fb_primary))
+ strokeColor = ColorStateList.valueOf(color(R.color.fb_glass_highlight))
+ strokeWidth = dp(1)
+ elevation = dp(3).toFloat()
setOnClickListener(action)
}
}
@@ -919,21 +1525,33 @@ class FridaBoxActivity : AppCompatActivity() {
text = label
isAllCaps = false
letterSpacing = 0f
- cornerRadius = dp(15)
+ cornerRadius = dp(18)
insetTop = 0
insetBottom = 0
setTextColor(color(R.color.fb_text_primary))
- strokeColor = ColorStateList.valueOf(color(R.color.fb_outline))
- backgroundTintList = ColorStateList.valueOf(color(R.color.fb_transparent))
+ strokeColor = ColorStateList.valueOf(color(R.color.fb_glass_outline))
+ backgroundTintList = ColorStateList.valueOf(color(R.color.fb_glass_surface_soft))
setOnClickListener(action)
}
}
+ private fun iconButton(iconResource: Int, description: String, action: () -> Unit): MaterialButton {
+ return outlineButton("") { action() }.apply {
+ contentDescription = description
+ icon = ContextCompat.getDrawable(this@FridaBoxActivity, iconResource)
+ iconTint = ColorStateList.valueOf(color(R.color.fb_text_primary))
+ iconPadding = 0
+ iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
+ minWidth = 0
+ setPadding(dp(8), 0, dp(8), 0)
+ }
+ }
+
private fun resetScreen(navId: Int, showImport: Boolean): Int {
screenGeneration += 1
binding.content.removeAllViews()
binding.contentScroll.scrollTo(0, 0)
- binding.importFab.isVisible = showImport
+ binding.importFabIcon.isVisible = showImport
changingNavigation = true
binding.bottomNavigation.selectedItemId = navId
changingNavigation = false
@@ -976,9 +1594,9 @@ class FridaBoxActivity : AppCompatActivity() {
private fun modeColor(mode: String, background: Boolean): Int {
val resource = when (mode) {
- InstrumentationSettings.MODE_LOCAL_SCRIPT -> if (background) R.color.fb_surface_tint else R.color.fb_success
- InstrumentationSettings.MODE_CLEAN -> if (background) R.color.fb_surface_tint else R.color.fb_text_secondary
- else -> if (background) R.color.fb_surface_tint else R.color.fb_warning
+ InstrumentationSettings.MODE_LOCAL_SCRIPT -> if (background) R.color.fb_glass_surface_soft else R.color.fb_success
+ InstrumentationSettings.MODE_CLEAN -> if (background) R.color.fb_glass_surface_soft else R.color.fb_text_secondary
+ else -> if (background) R.color.fb_glass_surface_soft else R.color.fb_warning
}
return color(resource)
}
@@ -1014,15 +1632,47 @@ class FridaBoxActivity : AppCompatActivity() {
private fun space(width: Int, height: Int) = Space(this).apply {
layoutParams = LinearLayout.LayoutParams(width, height)
}
- private fun notify(message: String) = Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
+ private fun notify(message: String) {
+ Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).apply {
+ setTextColor(color(R.color.fb_text_primary))
+ view.background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_glass_popup)
+ view.elevation = dp(12).toFloat()
+ show()
+ }
+ }
private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show()
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
+ private fun humanSize(bytes: Long): String {
+ if (bytes < 1024L) return "$bytes B"
+ val units = arrayOf("KiB", "MiB", "GiB")
+ var value = bytes.toDouble()
+ var unit = -1
+ while (value >= 1024.0 && unit < units.lastIndex) {
+ value /= 1024.0
+ unit++
+ }
+ return String.format(Locale.ROOT, "%.1f %s", value, units[unit])
+ }
+
+ private fun installedPackageSha256(apks: List): String {
+ val digest = MessageDigest.getInstance("SHA-256")
+ apks.sortedBy { it.name }.forEach { apk ->
+ digest.update(apk.name.toByteArray(Charsets.UTF_8))
+ digest.update(byteArrayOf(0))
+ apk.inputStream().use { input ->
+ val buffer = ByteArray(128 * 1024)
+ while (true) {
+ val count = input.read(buffer)
+ if (count < 0) break
+ digest.update(buffer, 0, count)
+ }
+ }
+ }
+ return digest.digest().joinToString("") { "%02x".format(it) }
+ }
+
companion object {
private const val MAX_AGENT_SIZE = 16L * 1024L * 1024L
- private const val MENU_DETAILS = 1
- private const val MENU_RUNTIME = 2
- private const val MENU_CLEAR = 3
- private const val MENU_REMOVE = 4
}
}
diff --git a/app/src/main/java/com/qm4rs/fridabox/GadgetManager.kt b/app/src/main/java/com/qm4rs/fridabox/GadgetManager.kt
new file mode 100644
index 0000000..38f3d7e
--- /dev/null
+++ b/app/src/main/java/com/qm4rs/fridabox/GadgetManager.kt
@@ -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,
+ 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 {
+ 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 { 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 {
+ val releases = JSONArray(json)
+ val result = ArrayList()
+ 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 }
+ }
+}
diff --git a/app/src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so b/app/src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so
deleted file mode 100644
index 6b8b54f..0000000
--- a/app/src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so
+++ /dev/null
@@ -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"
-}
diff --git a/app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so b/app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so
deleted file mode 100644
index b7829a8ec6e0d7d1ddfdc257d01d9aa192d6b94a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 25212656
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
qh8q6Aq2dx~;$F~j(?ApVgQhnN
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