commit 3d87e492b2d6612348eb6585a86e3973d37ef74e Author: sssnake Date: Fri Apr 3 06:38:12 2026 -0700 Driver Manager v2.0.0 - LSPosed module for per-app driver management diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13effcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.claude/ +.claude* +*.apk +*.iml +.idea/ +.gradle/ +build/ +local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3348cce --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "com.seteclabs.drivermanager" + compileSdk = 35 + + defaultConfig { + applicationId = "com.seteclabs.drivermanager" + minSdk = 29 + targetSdk = 35 + versionCode = 200 + versionName = "2.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + // Xposed API - compileOnly so it's not packaged (provided by LSPosed at runtime) + compileOnly("de.robv.android.xposed:api:82") + compileOnly("de.robv.android.xposed:api:82:sources") + + // AndroidX + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.cardview:cardview:1.0.0") + implementation("androidx.preference:preference:1.2.1") + implementation("com.google.code.gson:gson:2.11.0") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..68a11a6 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init new file mode 100644 index 0000000..6e4cdfe --- /dev/null +++ b/app/src/main/assets/xposed_init @@ -0,0 +1 @@ +com.seteclabs.drivermanager.xposed.DriverHook diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/ConfigProvider.java b/app/src/main/java/com/seteclabs/drivermanager/manager/ConfigProvider.java new file mode 100644 index 0000000..2ccf350 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/ConfigProvider.java @@ -0,0 +1,34 @@ +package com.seteclabs.drivermanager.manager; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/** + * Placeholder ContentProvider for future config sharing. + * Currently the Xposed hook reads config files directly from + * /data/local/tmp/driver-manager/scopes/ (set up by root). + */ +public class ConfigProvider extends ContentProvider { + + @Override + public boolean onCreate() { return true; } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { return null; } + + @Override + public String getType(Uri uri) { return null; } + + @Override + public Uri insert(Uri uri, ContentValues values) { return null; } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { return 0; } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/DriverRegistry.java b/app/src/main/java/com/seteclabs/drivermanager/manager/DriverRegistry.java new file mode 100644 index 0000000..e8fb31c --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/DriverRegistry.java @@ -0,0 +1,230 @@ +package com.seteclabs.drivermanager.manager; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.seteclabs.drivermanager.model.Driver; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the driver registry - discovers system drivers and tracks custom variants. + * Drivers are stored as JSON in the app's internal storage and synced to the + * shared config directory for the Xposed hook to read. + */ +public class DriverRegistry { + + private static final String REGISTRY_FILE = "drivers.json"; + private static final String CONFIG_DIR = "/data/local/tmp/driver-manager"; + + private final Context context; + private final Gson gson; + private List drivers = new ArrayList<>(); + + public DriverRegistry(Context context) { + this.context = context; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + /** + * Load drivers from storage. + */ + public void load() { + File file = new File(context.getFilesDir(), REGISTRY_FILE); + if (!file.exists()) return; + + try (FileReader reader = new FileReader(file)) { + Type type = new TypeToken>(){}.getType(); + List loaded = gson.fromJson(reader, type); + if (loaded != null) drivers = loaded; + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Save drivers to storage. + */ + public void save() { + File file = new File(context.getFilesDir(), REGISTRY_FILE); + try (FileWriter writer = new FileWriter(file)) { + gson.toJson(drivers, writer); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Scan the device for system drivers. + * Uses root to read /vendor, /system paths and compute hashes. + */ + public void scanSystem() { + drivers.clear(); + + // GPU drivers + scanDirectory("/vendor/lib64/egl", "gpu", "lib", ".so"); + scanDirectory("/vendor/lib64/hw", "gpu", "vulkan.", ".so"); + + // WiFi firmware + scanFirmware("/vendor/firmware", "wifi", "fw_bcm", ".bin"); + scanFirmware("/vendor/firmware", "wifi", "fw_wcn", ".bin"); + scanFirmware("/vendor/etc/wifi", "wifi", "", ".bin"); + + // Bluetooth firmware + scanFirmware("/vendor/firmware", "bluetooth", "BCM", ".hcd"); + scanFirmware("/vendor/firmware", "bluetooth", "BTFW", ".hcd"); + scanFirmware("/vendor/etc/bluetooth", "bluetooth", "", ".hcd"); + + // Audio HAL + scanDirectory("/vendor/lib64/hw", "audio", "audio", ".so"); + + // Camera HAL + scanDirectory("/vendor/lib64/hw", "camera", "camera", ".so"); + + // USB libraries (for SDR, etc.) + scanDirectory("/vendor/lib64", "usb", "libusb", ".so"); + scanDirectory("/system/lib64", "usb", "libusb", ".so"); + + // SDR-specific (if installed via Termux or other means) + scanDirectory("/data/data/com.termux/files/usr/lib", "sdr", "librtlsdr", ".so"); + scanDirectory("/data/data/com.termux/files/usr/lib", "sdr", "libhackrf", ".so"); + scanDirectory("/data/data/com.termux/files/usr/lib", "sdr", "libairspy", ".so"); + + save(); + } + + private void scanDirectory(String dirPath, String category, String prefix, String suffix) { + String result = RootShell.exec("ls -1 " + dirPath + "/ 2>/dev/null"); + if (result.startsWith("ERROR") || result.isEmpty()) return; + + for (String filename : result.split("\n")) { + filename = filename.trim(); + if (filename.isEmpty()) continue; + if (!prefix.isEmpty() && !filename.startsWith(prefix)) continue; + if (!suffix.isEmpty() && !filename.endsWith(suffix)) continue; + + String fullPath = dirPath + "/" + filename; + String hash = RootShell.exec("sha256sum " + fullPath + " 2>/dev/null | cut -d' ' -f1").trim(); + + // Derive library name (strip lib prefix and .so suffix) + String libName = filename; + if (libName.startsWith("lib")) libName = libName.substring(3); + if (libName.endsWith(".so")) libName = libName.substring(0, libName.length() - 3); + + String id = category + "-" + libName; + Driver driver = new Driver(id, filename, category, libName); + driver.setStockPath(fullPath); + driver.setStockHash(hash); + driver.setDescription(category.toUpperCase() + " driver: " + filename); + + // Add stock as default variant + Driver.Variant stock = new Driver.Variant("stock", "Stock", fullPath); + stock.setHash(hash); + driver.getVariants().add(stock); + driver.setActiveVariant("stock"); + + drivers.add(driver); + } + } + + private void scanFirmware(String dirPath, String category, String prefix, String suffix) { + // Same as scanDirectory but for firmware files + scanDirectory(dirPath, category, prefix, suffix); + } + + /** + * Add a custom driver variant. + * Copies the file to the drivers directory and registers it. + */ + public boolean addVariant(String driverId, String variantName, String sourcePath) { + Driver driver = getDriver(driverId); + if (driver == null) return false; + + String filename = new File(sourcePath).getName(); + String destPath = CONFIG_DIR + "/drivers/" + driverId + "_" + variantName + "_" + filename; + + // Copy file to drivers dir with root + int code = RootShell.execCode("cp '" + sourcePath + "' '" + destPath + "' && chmod 644 '" + destPath + "'"); + if (code != 0) return false; + + String hash = RootShell.exec("sha256sum '" + destPath + "' | cut -d' ' -f1").trim(); + + Driver.Variant variant = new Driver.Variant(variantName, variantName, destPath); + variant.setHash(hash); + driver.getVariants().add(variant); + + save(); + return true; + } + + /** + * Remove a driver variant. + */ + public void removeVariant(String driverId, String variantId) { + Driver driver = getDriver(driverId); + if (driver == null) return; + + driver.getVariants().removeIf(v -> v.getId().equals(variantId) && !"stock".equals(v.getId())); + + if (variantId.equals(driver.getActiveVariant())) { + driver.setActiveVariant("stock"); + } + + save(); + } + + /** + * Set the active variant for a driver. + */ + public void setActiveVariant(String driverId, String variantId) { + Driver driver = getDriver(driverId); + if (driver == null) return; + driver.setActiveVariant(variantId); + save(); + } + + public Driver getDriver(String id) { + for (Driver d : drivers) { + if (d.getId().equals(id)) return d; + } + return null; + } + + public List getDrivers() { return drivers; } + + public List getDriversByCategory(String category) { + List result = new ArrayList<>(); + for (Driver d : drivers) { + if (d.getCategory().equals(category)) result.add(d); + } + return result; + } + + public List getEnabledDrivers() { + List result = new ArrayList<>(); + for (Driver d : drivers) { + if (d.isEnabled()) result.add(d); + } + return result; + } + + /** + * Verify a driver's current hash matches the registered stock hash. + */ + public String verifyDriver(String driverId) { + Driver driver = getDriver(driverId); + if (driver == null) return "not_found"; + + String currentHash = RootShell.exec("sha256sum '" + driver.getStockPath() + "' | cut -d' ' -f1").trim(); + if (currentHash.equals(driver.getStockHash())) return "ok"; + return "changed"; + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/KoManager.java b/app/src/main/java/com/seteclabs/drivermanager/manager/KoManager.java new file mode 100644 index 0000000..c4dce88 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/KoManager.java @@ -0,0 +1,186 @@ +package com.seteclabs.drivermanager.manager; + +import com.seteclabs.drivermanager.model.KernelModule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Manages .ko kernel modules via root shell. + * Modules are stored in /data/local/tmp/driver-manager/modules/ + */ +public class KoManager { + + private static final String MODULES_DIR = "/data/local/tmp/driver-manager/modules"; + private static final String AUTOLOAD_FILE = "/data/local/tmp/driver-manager/autoload.conf"; + + /** + * List all .ko files in the modules directory with their status. + */ + public List listModules() { + List modules = new ArrayList<>(); + + String files = RootShell.exec("ls -1 " + MODULES_DIR + "/*.ko 2>/dev/null"); + if (files.isEmpty() || files.startsWith("ERROR")) return modules; + + // Get currently loaded modules + String lsmod = RootShell.exec("lsmod 2>/dev/null"); + Set loadedModules = new HashSet<>(); + for (String line : lsmod.split("\n")) { + String name = line.split("\\s+")[0]; + if (!"Module".equals(name)) loadedModules.add(name); + } + + // Get autoload list + String autoloadContent = RootShell.exec("cat " + AUTOLOAD_FILE + " 2>/dev/null"); + Set autoloadSet = new HashSet<>(); + for (String line : autoloadContent.split("\n")) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) autoloadSet.add(line); + } + + // Get running kernel version + String kernelVer = RootShell.exec("uname -r").trim(); + + for (String filepath : files.split("\n")) { + filepath = filepath.trim(); + if (filepath.isEmpty()) continue; + + KernelModule mod = new KernelModule(); + String filename = filepath.substring(filepath.lastIndexOf('/') + 1); + mod.setFilename(filename); + + // Get modinfo + String info = RootShell.exec("modinfo " + filepath + " 2>/dev/null"); + for (String line : info.split("\n")) { + if (line.startsWith("name:")) mod.setName(line.substring(5).trim()); + else if (line.startsWith("description:")) mod.setDescription(line.substring(12).trim()); + else if (line.startsWith("version:")) mod.setVersion(line.substring(8).trim()); + else if (line.startsWith("author:")) mod.setAuthor(line.substring(7).trim()); + else if (line.startsWith("depends:")) mod.setDepends(line.substring(8).trim()); + else if (line.startsWith("vermagic:")) mod.setVermagic(line.substring(9).trim()); + } + + if (mod.getName() == null) mod.setName(filename.replace(".ko", "")); + + // File size + String size = RootShell.exec("stat -c%s " + filepath + " 2>/dev/null").trim(); + try { mod.setSize(Long.parseLong(size)); } catch (NumberFormatException e) { mod.setSize(0); } + + // Check if loaded + mod.setLoaded(loadedModules.contains(mod.getName())); + + // Check autoload + mod.setAutoload(autoloadSet.contains(filename)); + + // Check kernel version compatibility + if (mod.getVermagic() != null && !mod.getVermagic().isEmpty()) { + String modKver = mod.getVermagic().split("\\s+")[0]; + mod.setCompatible(modKver.equals(kernelVer)); + } + + modules.add(mod); + } + + return modules; + } + + /** + * Load a kernel module. + * @return result message + */ + public String loadModule(String filename, String params) { + String filepath = MODULES_DIR + "/" + filename; + + // Check if file exists + String exists = RootShell.exec("[ -f '" + filepath + "' ] && echo yes || echo no").trim(); + if (!"yes".equals(exists)) return "Module file not found: " + filename; + + // Load dependencies first + String depends = RootShell.exec("modinfo -F depends '" + filepath + "' 2>/dev/null").trim(); + if (!depends.isEmpty()) { + for (String dep : depends.split(",")) { + dep = dep.trim(); + if (dep.isEmpty()) continue; + String loaded = RootShell.exec("lsmod | grep -q '^" + dep + " ' && echo yes || echo no").trim(); + if (!"yes".equals(loaded)) { + String depFile = MODULES_DIR + "/" + dep + ".ko"; + String depExists = RootShell.exec("[ -f '" + depFile + "' ] && echo yes || echo no").trim(); + if ("yes".equals(depExists)) { + RootShell.exec("insmod '" + depFile + "'"); + } else { + RootShell.exec("modprobe '" + dep + "'"); + } + } + } + } + + String paramStr = params != null ? params : ""; + String result = RootShell.exec("insmod '" + filepath + "' " + paramStr + " 2>&1"); + if (result.isEmpty()) return "OK"; + return result; + } + + /** + * Unload a kernel module. + */ + public String unloadModule(String name) { + String result = RootShell.exec("rmmod '" + name + "' 2>&1"); + if (result.isEmpty()) return "OK"; + return result; + } + + /** + * Set autoload on/off for a module. + */ + public void setAutoload(String filename, boolean enabled) { + if (enabled) { + // Add to autoload if not already there + RootShell.exec("grep -qxF '" + filename + "' " + AUTOLOAD_FILE + + " || echo '" + filename + "' >> " + AUTOLOAD_FILE); + } else { + RootShell.exec("sed -i '/^" + filename + "$/d' " + AUTOLOAD_FILE); + } + } + + /** + * Load all modules in the autoload list. + */ + public void autoloadAll() { + String content = RootShell.exec("cat " + AUTOLOAD_FILE + " 2>/dev/null"); + for (String line : content.split("\n")) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + loadModule(line, null); + } + } + + /** + * Unload all managed modules. + */ + public void unloadAll() { + List modules = listModules(); + for (KernelModule mod : modules) { + if (mod.isLoaded()) { + unloadModule(mod.getName()); + } + } + } + + /** + * Get detailed modinfo output for a module. + */ + public String getModuleInfo(String filename) { + return RootShell.exec("modinfo " + MODULES_DIR + "/" + filename + " 2>/dev/null"); + } + + /** + * Get recent dmesg output related to module loading. + */ + public String getModuleDmesg() { + return RootShell.exec("dmesg | tail -30 2>/dev/null"); + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/ProtectionManager.java b/app/src/main/java/com/seteclabs/drivermanager/manager/ProtectionManager.java new file mode 100644 index 0000000..973408e --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/ProtectionManager.java @@ -0,0 +1,153 @@ +package com.seteclabs.drivermanager.manager; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.seteclabs.drivermanager.model.Driver; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Monitors system driver integrity by comparing current hashes against a baseline. + * Does NOT modify system files. Uses root to read and hash files. + */ +public class ProtectionManager { + + private static final String BASELINE_FILE = "baseline.json"; + private static final String BACKUP_DIR = "/data/local/tmp/driver-manager/backup"; + + private final Context context; + private final DriverRegistry registry; + private final Gson gson; + + /** Map of file path -> expected SHA256 hash */ + private Map baseline = new HashMap<>(); + + public ProtectionManager(Context context, DriverRegistry registry) { + this.context = context; + this.registry = registry; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + loadBaseline(); + } + + /** + * Create a baseline snapshot of all protected drivers. + */ + public int createBaseline() { + baseline.clear(); + int count = 0; + + for (Driver driver : registry.getDrivers()) { + if (!driver.isEnabled()) continue; // Only baseline enabled/protected drivers + String path = driver.getStockPath(); + if (path == null || path.isEmpty()) continue; + + String hash = RootShell.exec("sha256sum '" + path + "' 2>/dev/null | cut -d' ' -f1").trim(); + if (!hash.isEmpty() && !hash.startsWith("ERROR")) { + baseline.put(path, hash); + count++; + + // Backup the file + String backupPath = BACKUP_DIR + path; + String backupDir = backupPath.substring(0, backupPath.lastIndexOf('/')); + RootShell.exec("mkdir -p '" + backupDir + "' && cp -p '" + path + "' '" + backupPath + "'"); + } + } + + saveBaseline(); + return count; + } + + /** + * Check all baselined files for changes. + * @return list of check results + */ + public List checkIntegrity() { + List results = new ArrayList<>(); + + for (Map.Entry entry : baseline.entrySet()) { + String path = entry.getKey(); + String expectedHash = entry.getValue(); + + CheckResult result = new CheckResult(); + result.path = path; + result.expectedHash = expectedHash; + + String exists = RootShell.exec("[ -f '" + path + "' ] && echo yes || echo no").trim(); + if (!"yes".equals(exists)) { + result.status = "missing"; + result.actualHash = ""; + } else { + result.actualHash = RootShell.exec("sha256sum '" + path + "' | cut -d' ' -f1").trim(); + result.status = result.actualHash.equals(expectedHash) ? "ok" : "changed"; + } + + results.add(result); + } + + return results; + } + + /** + * Get summary stats. + */ + public Stats getStats() { + Stats stats = new Stats(); + stats.totalProtected = baseline.size(); + + List results = checkIntegrity(); + for (CheckResult r : results) { + switch (r.status) { + case "ok": stats.ok++; break; + case "changed": stats.changed++; break; + case "missing": stats.missing++; break; + } + } + return stats; + } + + private void loadBaseline() { + File file = new File(context.getFilesDir(), BASELINE_FILE); + if (!file.exists()) return; + + try (FileReader reader = new FileReader(file)) { + Type type = new TypeToken>(){}.getType(); + Map loaded = gson.fromJson(reader, type); + if (loaded != null) baseline = loaded; + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void saveBaseline() { + File file = new File(context.getFilesDir(), BASELINE_FILE); + try (FileWriter writer = new FileWriter(file)) { + gson.toJson(baseline, writer); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static class CheckResult { + public String path; + public String expectedHash; + public String actualHash; + public String status; // ok, changed, missing + } + + public static class Stats { + public int totalProtected; + public int ok; + public int changed; + public int missing; + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/RootShell.java b/app/src/main/java/com/seteclabs/drivermanager/manager/RootShell.java new file mode 100644 index 0000000..200d5b7 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/RootShell.java @@ -0,0 +1,74 @@ +package com.seteclabs.drivermanager.manager; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; + +/** + * Execute commands via su (KernelSU/Magisk root). + * Used for operations that need root: .ko loading, file hashing, driver scanning. + */ +public class RootShell { + + /** + * Execute a command as root and return stdout. + */ + public static String exec(String command) { + StringBuilder output = new StringBuilder(); + try { + Process process = Runtime.getRuntime().exec("su"); + DataOutputStream os = new DataOutputStream(process.getOutputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + os.writeBytes(command + "\n"); + os.writeBytes("exit\n"); + os.flush(); + + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + + process.waitFor(); + reader.close(); + os.close(); + } catch (Exception e) { + return "ERROR: " + e.getMessage(); + } + return output.toString().trim(); + } + + /** + * Execute a command as root, return exit code. + */ + public static int execCode(String command) { + try { + Process process = Runtime.getRuntime().exec("su"); + DataOutputStream os = new DataOutputStream(process.getOutputStream()); + os.writeBytes(command + "\n"); + os.writeBytes("exit\n"); + os.flush(); + return process.waitFor(); + } catch (Exception e) { + return -1; + } + } + + /** + * Check if root is available. + */ + public static boolean hasRoot() { + return execCode("id") == 0; + } + + /** + * Ensure the config directory structure exists. + */ + public static void ensureConfigDirs() { + exec("mkdir -p /data/local/tmp/driver-manager/scopes"); + exec("mkdir -p /data/local/tmp/driver-manager/drivers"); + exec("mkdir -p /data/local/tmp/driver-manager/modules"); + exec("mkdir -p /data/local/tmp/driver-manager/backup"); + exec("chmod -R 755 /data/local/tmp/driver-manager"); + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/manager/ScopeManager.java b/app/src/main/java/com/seteclabs/drivermanager/manager/ScopeManager.java new file mode 100644 index 0000000..05a1a21 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/manager/ScopeManager.java @@ -0,0 +1,264 @@ +package com.seteclabs.drivermanager.manager; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.seteclabs.drivermanager.model.Driver; +import com.seteclabs.drivermanager.model.Scope; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Manages per-app driver scoping. + * + * The scope config is written to /data/local/tmp/driver-manager/scopes/{package}.json + * which the Xposed hook reads at app startup to know what to redirect. + * + * System drivers are NEVER modified. Only the in-process library loading path + * is redirected for scoped apps. + */ +public class ScopeManager { + + private static final String CONFIG_DIR = "/data/local/tmp/driver-manager"; + private static final String SCOPES_DIR = CONFIG_DIR + "/scopes"; + private static final String SCOPE_MAP_FILE = "scope_map.json"; + + private final Context context; + private final Gson gson; + private final DriverRegistry registry; + + /** + * Master scope map: driverId -> set of package names. + * This is the UI-friendly view. We convert this to per-package scope files + * that the Xposed hook reads. + */ + private Map> scopeMap = new HashMap<>(); + + public ScopeManager(Context context, DriverRegistry registry) { + this.context = context; + this.registry = registry; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + /** + * Load the master scope map from app storage. + */ + public void load() { + File file = new File(context.getFilesDir(), SCOPE_MAP_FILE); + if (!file.exists()) return; + + try (FileReader reader = new FileReader(file)) { + Type type = new TypeToken>>(){}.getType(); + Map> loaded = gson.fromJson(reader, type); + if (loaded != null) scopeMap = loaded; + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Save the master scope map. + */ + private void saveMaster() { + File file = new File(context.getFilesDir(), SCOPE_MAP_FILE); + try (FileWriter writer = new FileWriter(file)) { + gson.toJson(scopeMap, writer); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Add an app to a driver's scope. + */ + public void addAppToScope(String driverId, String packageName) { + scopeMap.computeIfAbsent(driverId, k -> new HashSet<>()).add(packageName); + saveMaster(); + writePerPackageConfig(packageName); + } + + /** + * Remove an app from a driver's scope. + */ + public void removeAppFromScope(String driverId, String packageName) { + Set apps = scopeMap.get(driverId); + if (apps != null) { + apps.remove(packageName); + if (apps.isEmpty()) scopeMap.remove(driverId); + } + saveMaster(); + writePerPackageConfig(packageName); + } + + /** + * Check if an app is scoped for a driver. + */ + public boolean isAppScoped(String driverId, String packageName) { + Set apps = scopeMap.get(driverId); + return apps != null && apps.contains(packageName); + } + + /** + * Get all packages scoped for a driver. + */ + public Set getScopedApps(String driverId) { + return scopeMap.getOrDefault(driverId, new HashSet<>()); + } + + /** + * Get all drivers scoped for a package. + */ + public List getScopedDrivers(String packageName) { + List result = new ArrayList<>(); + for (Map.Entry> entry : scopeMap.entrySet()) { + if (entry.getValue().contains(packageName)) { + result.add(entry.getKey()); + } + } + return result; + } + + /** + * Set scope for all apps at once (system-wide for a driver). + * Adds every installed non-system app to the scope. + */ + public void setScopeSystemWide(String driverId) { + Set allApps = new HashSet<>(); + PackageManager pm = context.getPackageManager(); + for (ApplicationInfo ai : pm.getInstalledApplications(0)) { + if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + allApps.add(ai.packageName); + } + } + scopeMap.put(driverId, allApps); + saveMaster(); + syncAllConfigs(); + } + + /** + * Clear all scopes for a driver. + */ + public void clearScope(String driverId) { + Set oldApps = scopeMap.remove(driverId); + saveMaster(); + if (oldApps != null) { + for (String pkg : oldApps) { + writePerPackageConfig(pkg); + } + } + } + + /** + * Write the per-package scope config file that the Xposed hook reads. + * + * For each scoped package, we create: + * /data/local/tmp/driver-manager/scopes/{package}.json + * containing all library and file redirects for that package. + */ + private void writePerPackageConfig(String packageName) { + Scope scope = new Scope(); + + // Build the redirect map from all drivers scoped to this package + for (Map.Entry> entry : scopeMap.entrySet()) { + if (!entry.getValue().contains(packageName)) continue; + + String driverId = entry.getKey(); + Driver driver = registry.getDriver(driverId); + if (driver == null || !driver.isEnabled()) continue; + + String customPath = driver.getActiveVariantPath(); + if (customPath == null || "stock".equals(driver.getActiveVariant())) continue; + + // Library redirect (for System.loadLibrary hooks) + if (driver.getLibraryName() != null && !driver.getLibraryName().isEmpty()) { + scope.addLibrary(driver.getLibraryName(), customPath); + } + + // File redirect (for direct file access hooks) + if (driver.getStockPath() != null) { + scope.addFile(driver.getStockPath(), customPath); + } + } + + String scopeFilePath = SCOPES_DIR + "/" + packageName + ".json"; + + if (scope.isEmpty()) { + // No redirects for this package, remove the config file + RootShell.exec("rm -f '" + scopeFilePath + "'"); + } else { + // Write the config file + String json = gson.toJson(scope); + RootShell.exec("echo '" + json.replace("'", "'\\''") + "' > '" + scopeFilePath + "'"); + RootShell.exec("chmod 644 '" + scopeFilePath + "'"); + } + } + + /** + * Rebuild all per-package config files. + * Call after changing driver variants or enabling/disabling drivers. + */ + public void syncAllConfigs() { + // Collect all unique packages + Set allPackages = new HashSet<>(); + for (Set apps : scopeMap.values()) { + allPackages.addAll(apps); + } + + // Clear old configs + RootShell.exec("rm -f " + SCOPES_DIR + "/*.json"); + + // Write new configs + for (String pkg : allPackages) { + writePerPackageConfig(pkg); + } + } + + /** + * Get installed apps for the scope selection UI. + */ + public List getInstalledApps() { + List result = new ArrayList<>(); + PackageManager pm = context.getPackageManager(); + + for (ApplicationInfo ai : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + AppInfo info = new AppInfo(); + info.packageName = ai.packageName; + info.label = pm.getApplicationLabel(ai).toString(); + info.isSystem = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + info.icon = ai; + result.add(info); + } + + // Sort: user apps first, then alphabetically + result.sort((a, b) -> { + if (a.isSystem != b.isSystem) return a.isSystem ? 1 : -1; + return a.label.compareToIgnoreCase(b.label); + }); + + return result; + } + + /** + * App info for the scope selection UI. + */ + public static class AppInfo { + public String packageName; + public String label; + public boolean isSystem; + public ApplicationInfo icon; + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/model/Driver.java b/app/src/main/java/com/seteclabs/drivermanager/model/Driver.java new file mode 100644 index 0000000..b434a9a --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/model/Driver.java @@ -0,0 +1,93 @@ +package com.seteclabs.drivermanager.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a driver that can be managed (redirected per-app). + */ +public class Driver { + private String id; + private String name; + private String category; // gpu, wifi, bluetooth, audio, sdr, usb, misc + private String description; + private String stockPath; // Original system path (e.g., /vendor/lib64/egl/libEGL_powervr.so) + private String stockHash; // SHA256 of stock file + private String libraryName; // Name used in System.loadLibrary() (e.g., "rtlsdr") + private boolean enabled; + private List variants = new ArrayList<>(); + private String activeVariant; // ID of currently selected variant + + public Driver() {} + + public Driver(String id, String name, String category, String libraryName) { + this.id = id; + this.name = name; + this.category = category; + this.libraryName = libraryName; + this.enabled = false; + } + + // --- Getters/Setters --- + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getStockPath() { return stockPath; } + public void setStockPath(String stockPath) { this.stockPath = stockPath; } + public String getStockHash() { return stockHash; } + public void setStockHash(String stockHash) { this.stockHash = stockHash; } + public String getLibraryName() { return libraryName; } + public void setLibraryName(String libraryName) { this.libraryName = libraryName; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public List getVariants() { return variants; } + public void setVariants(List variants) { this.variants = variants; } + public String getActiveVariant() { return activeVariant; } + public void setActiveVariant(String activeVariant) { this.activeVariant = activeVariant; } + + /** + * Get the path of the currently active variant. + */ + public String getActiveVariantPath() { + if (activeVariant == null) return null; + for (Variant v : variants) { + if (v.getId().equals(activeVariant)) return v.getPath(); + } + return null; + } + + /** + * A driver variant (e.g., stock, custom v1, custom v2). + */ + public static class Variant { + private String id; + private String name; + private String path; + private String hash; + private long size; + + public Variant() {} + + public Variant(String id, String name, String path) { + this.id = id; + this.name = name; + this.path = path; + } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + public String getHash() { return hash; } + public void setHash(String hash) { this.hash = hash; } + public long getSize() { return size; } + public void setSize(long size) { this.size = size; } + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/model/KernelModule.java b/app/src/main/java/com/seteclabs/drivermanager/model/KernelModule.java new file mode 100644 index 0000000..9c3770b --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/model/KernelModule.java @@ -0,0 +1,43 @@ +package com.seteclabs.drivermanager.model; + +/** + * Represents a .ko kernel module file. + */ +public class KernelModule { + private String filename; + private String name; + private String description; + private String version; + private String author; + private String depends; + private String vermagic; + private long size; + private boolean loaded; + private boolean autoload; + private boolean compatible; + + public KernelModule() {} + + public String getFilename() { return filename; } + public void setFilename(String filename) { this.filename = filename; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getVersion() { return version; } + public void setVersion(String version) { this.version = version; } + public String getAuthor() { return author; } + public void setAuthor(String author) { this.author = author; } + public String getDepends() { return depends; } + public void setDepends(String depends) { this.depends = depends; } + public String getVermagic() { return vermagic; } + public void setVermagic(String vermagic) { this.vermagic = vermagic; } + public long getSize() { return size; } + public void setSize(long size) { this.size = size; } + public boolean isLoaded() { return loaded; } + public void setLoaded(boolean loaded) { this.loaded = loaded; } + public boolean isAutoload() { return autoload; } + public void setAutoload(boolean autoload) { this.autoload = autoload; } + public boolean isCompatible() { return compatible; } + public void setCompatible(boolean compatible) { this.compatible = compatible; } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/model/Scope.java b/app/src/main/java/com/seteclabs/drivermanager/model/Scope.java new file mode 100644 index 0000000..31515d0 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/model/Scope.java @@ -0,0 +1,43 @@ +package com.seteclabs.drivermanager.model; + +import java.util.HashMap; +import java.util.Map; + +/** + * Per-app scope configuration. + * Defines which driver libraries and files get redirected for a specific package. + */ +public class Scope { + /** Map of library name -> custom .so path */ + private Map libraries = new HashMap<>(); + + /** Map of original file path -> custom file path */ + private Map files = new HashMap<>(); + + public Scope() {} + + public Map getLibraries() { return libraries; } + public void setLibraries(Map libraries) { this.libraries = libraries; } + public Map getFiles() { return files; } + public void setFiles(Map files) { this.files = files; } + + public void addLibrary(String name, String customPath) { + libraries.put(name, customPath); + } + + public void removeLibrary(String name) { + libraries.remove(name); + } + + public void addFile(String originalPath, String customPath) { + files.put(originalPath, customPath); + } + + public void removeFile(String originalPath) { + files.remove(originalPath); + } + + public boolean isEmpty() { + return libraries.isEmpty() && files.isEmpty(); + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/ui/AppsFragment.java b/app/src/main/java/com/seteclabs/drivermanager/ui/AppsFragment.java new file mode 100644 index 0000000..4264592 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/ui/AppsFragment.java @@ -0,0 +1,212 @@ +package com.seteclabs.drivermanager.ui; + +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.seteclabs.drivermanager.R; +import com.seteclabs.drivermanager.manager.DriverRegistry; +import com.seteclabs.drivermanager.manager.ScopeManager; +import com.seteclabs.drivermanager.model.Driver; + +import java.util.ArrayList; +import java.util.List; + +/** + * LSPosed-style per-app scope selection. + * + * 1. User selects a driver from the dropdown + * 2. Scrollable list of all installed apps appears + * 3. User checks which apps should use the custom driver variant + * 4. The Xposed hook reads this config and redirects library loading + */ +public class AppsFragment extends Fragment { + + private DriverRegistry driverRegistry; + private ScopeManager scopeManager; + private RecyclerView recyclerView; + private AppAdapter adapter; + private Spinner driverSpinner; + private EditText searchBar; + + private List enabledDrivers = new ArrayList<>(); + private List allApps = new ArrayList<>(); + private List filteredApps = new ArrayList<>(); + private String selectedDriverId = null; + + public static AppsFragment newInstance() { + return new AppsFragment(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_apps, container, false); + + driverRegistry = ((MainActivity) requireActivity()).getDriverRegistry(); + scopeManager = ((MainActivity) requireActivity()).getScopeManager(); + + // Driver selector dropdown + driverSpinner = view.findViewById(R.id.driver_spinner); + setupDriverSpinner(); + + // Search bar + searchBar = view.findViewById(R.id.search_bar); + searchBar.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override + public void afterTextChanged(Editable s) { + filterApps(s.toString()); + } + }); + + // App list + recyclerView = view.findViewById(R.id.app_list); + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + adapter = new AppAdapter(); + recyclerView.setAdapter(adapter); + + // Load apps in background + new Thread(() -> { + allApps = scopeManager.getInstalledApps(); + filteredApps = new ArrayList<>(allApps); + requireActivity().runOnUiThread(() -> adapter.notifyDataSetChanged()); + }).start(); + + return view; + } + + private void setupDriverSpinner() { + enabledDrivers = driverRegistry.getEnabledDrivers(); + + List driverNames = new ArrayList<>(); + driverNames.add("Select a driver to scope..."); + for (Driver d : enabledDrivers) { + driverNames.add("[" + d.getCategory().toUpperCase() + "] " + d.getName()); + } + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>( + requireContext(), android.R.layout.simple_spinner_item, driverNames); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + driverSpinner.setAdapter(spinnerAdapter); + + driverSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + if (pos == 0) { + selectedDriverId = null; + } else { + selectedDriverId = enabledDrivers.get(pos - 1).getId(); + } + adapter.notifyDataSetChanged(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + private void filterApps(String query) { + query = query.toLowerCase(); + filteredApps.clear(); + for (ScopeManager.AppInfo app : allApps) { + if (app.label.toLowerCase().contains(query) + || app.packageName.toLowerCase().contains(query)) { + filteredApps.add(app); + } + } + adapter.notifyDataSetChanged(); + } + + /** + * RecyclerView adapter for the app list with scope checkboxes. + */ + class AppAdapter extends RecyclerView.Adapter { + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_app, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ScopeManager.AppInfo app = filteredApps.get(position); + + holder.appName.setText(app.label); + holder.appPackage.setText(app.packageName); + + // Load app icon + try { + Drawable icon = requireContext().getPackageManager() + .getApplicationIcon(app.packageName); + holder.appIcon.setImageDrawable(icon); + } catch (PackageManager.NameNotFoundException e) { + holder.appIcon.setImageResource(android.R.drawable.sym_def_app_icon); + } + + // System app indicator + holder.systemBadge.setVisibility(app.isSystem ? View.VISIBLE : View.GONE); + + // Scope checkbox + holder.scopeCheck.setOnCheckedChangeListener(null); + if (selectedDriverId != null) { + holder.scopeCheck.setEnabled(true); + holder.scopeCheck.setChecked(scopeManager.isAppScoped(selectedDriverId, app.packageName)); + holder.scopeCheck.setOnCheckedChangeListener((v, checked) -> { + if (checked) { + scopeManager.addAppToScope(selectedDriverId, app.packageName); + } else { + scopeManager.removeAppFromScope(selectedDriverId, app.packageName); + } + }); + } else { + holder.scopeCheck.setEnabled(false); + holder.scopeCheck.setChecked(false); + } + + // Click entire row to toggle + holder.itemView.setOnClickListener(v -> { + if (selectedDriverId != null) { + holder.scopeCheck.toggle(); + } + }); + } + + @Override + public int getItemCount() { return filteredApps.size(); } + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView appIcon; + TextView appName, appPackage, systemBadge; + CheckBox scopeCheck; + + ViewHolder(View v) { + super(v); + appIcon = v.findViewById(R.id.app_icon); + appName = v.findViewById(R.id.app_name); + appPackage = v.findViewById(R.id.app_package); + systemBadge = v.findViewById(R.id.system_badge); + scopeCheck = v.findViewById(R.id.scope_check); + } + } + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/ui/DriversFragment.java b/app/src/main/java/com/seteclabs/drivermanager/ui/DriversFragment.java new file mode 100644 index 0000000..dbf8d9c --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/ui/DriversFragment.java @@ -0,0 +1,176 @@ +package com.seteclabs.drivermanager.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.seteclabs.drivermanager.R; +import com.seteclabs.drivermanager.manager.DriverRegistry; +import com.seteclabs.drivermanager.model.Driver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Displays all discovered drivers with category filtering. + * Users can enable/disable drivers and select active variants. + */ +public class DriversFragment extends Fragment { + + private DriverRegistry registry; + private RecyclerView recyclerView; + private DriverAdapter adapter; + private String currentFilter = "all"; + + public static DriversFragment newInstance() { + return new DriversFragment(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_drivers, container, false); + + registry = ((MainActivity) requireActivity()).getDriverRegistry(); + + // Category filter chips + ChipGroup chipGroup = view.findViewById(R.id.category_chips); + String[] categories = {"all", "gpu", "wifi", "bluetooth", "audio", "camera", "usb", "sdr"}; + for (String cat : categories) { + Chip chip = new Chip(requireContext()); + chip.setText(cat.toUpperCase(Locale.ROOT)); + chip.setCheckable(true); + chip.setChecked("all".equals(cat)); + chip.setOnCheckedChangeListener((v, checked) -> { + if (checked) { + currentFilter = cat; + refreshList(); + } + }); + chipGroup.addView(chip); + } + + // Driver list + recyclerView = view.findViewById(R.id.driver_list); + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + adapter = new DriverAdapter(); + recyclerView.setAdapter(adapter); + + // Scan button + FloatingActionButton fab = view.findViewById(R.id.fab_scan); + fab.setOnClickListener(v -> { + Toast.makeText(requireContext(), "Scanning drivers...", Toast.LENGTH_SHORT).show(); + new Thread(() -> { + registry.scanSystem(); + requireActivity().runOnUiThread(this::refreshList); + }).start(); + }); + + refreshList(); + return view; + } + + private void refreshList() { + List drivers; + if ("all".equals(currentFilter)) { + drivers = registry.getDrivers(); + } else { + drivers = registry.getDriversByCategory(currentFilter); + } + adapter.setDrivers(drivers); + } + + /** + * RecyclerView adapter for driver items. + */ + class DriverAdapter extends RecyclerView.Adapter { + private List drivers = new ArrayList<>(); + + void setDrivers(List drivers) { + this.drivers = drivers; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_driver, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Driver driver = drivers.get(position); + holder.name.setText(driver.getName()); + holder.category.setText(driver.getCategory().toUpperCase(Locale.ROOT)); + holder.path.setText(driver.getStockPath() != null ? driver.getStockPath() : ""); + + int variantCount = driver.getVariants() != null ? driver.getVariants().size() : 0; + holder.variants.setText(variantCount + " variant" + (variantCount != 1 ? "s" : "")); + + holder.toggle.setOnCheckedChangeListener(null); + holder.toggle.setChecked(driver.isEnabled()); + holder.toggle.setOnCheckedChangeListener((v, checked) -> { + driver.setEnabled(checked); + registry.save(); + }); + + // Click to show variant selector + holder.itemView.setOnClickListener(v -> { + showVariantSelector(driver); + }); + } + + @Override + public int getItemCount() { return drivers.size(); } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView name, category, path, variants; + SwitchMaterial toggle; + + ViewHolder(View v) { + super(v); + name = v.findViewById(R.id.driver_name); + category = v.findViewById(R.id.driver_category); + path = v.findViewById(R.id.driver_path); + variants = v.findViewById(R.id.driver_variants); + toggle = v.findViewById(R.id.driver_toggle); + } + } + } + + private void showVariantSelector(Driver driver) { + // TODO: Show a bottom sheet with variant list and option to add new variant + StringBuilder info = new StringBuilder(); + info.append(driver.getName()).append("\n"); + info.append("Category: ").append(driver.getCategory()).append("\n"); + info.append("Library: ").append(driver.getLibraryName()).append("\n"); + info.append("Stock: ").append(driver.getStockPath()).append("\n"); + info.append("Hash: ").append(driver.getStockHash()).append("\n\n"); + info.append("Variants:\n"); + for (Driver.Variant v : driver.getVariants()) { + String active = v.getId().equals(driver.getActiveVariant()) ? " [ACTIVE]" : ""; + info.append(" - ").append(v.getName()).append(active).append("\n"); + info.append(" ").append(v.getPath()).append("\n"); + } + + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Driver Details") + .setMessage(info.toString()) + .setPositiveButton("OK", null) + .show(); + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/ui/MainActivity.java b/app/src/main/java/com/seteclabs/drivermanager/ui/MainActivity.java new file mode 100644 index 0000000..6e849b9 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/ui/MainActivity.java @@ -0,0 +1,75 @@ +package com.seteclabs.drivermanager.ui; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.seteclabs.drivermanager.R; +import com.seteclabs.drivermanager.manager.DriverRegistry; +import com.seteclabs.drivermanager.manager.KoManager; +import com.seteclabs.drivermanager.manager.ProtectionManager; +import com.seteclabs.drivermanager.manager.RootShell; +import com.seteclabs.drivermanager.manager.ScopeManager; + +/** + * Main activity with bottom navigation. + * Tabs: Drivers, Apps (scope), Modules (.ko), Protection + */ +public class MainActivity extends AppCompatActivity { + + private DriverRegistry driverRegistry; + private ScopeManager scopeManager; + private KoManager koManager; + private ProtectionManager protectionManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Initialize root and config directories + RootShell.ensureConfigDirs(); + + // Initialize managers + driverRegistry = new DriverRegistry(this); + scopeManager = new ScopeManager(this, driverRegistry); + koManager = new KoManager(); + protectionManager = new ProtectionManager(this, driverRegistry); + + // Bottom navigation + BottomNavigationView nav = findViewById(R.id.bottom_nav); + nav.setOnItemSelectedListener(item -> { + Fragment fragment = null; + int id = item.getItemId(); + + if (id == R.id.nav_drivers) { + fragment = DriversFragment.newInstance(); + } else if (id == R.id.nav_apps) { + fragment = AppsFragment.newInstance(); + } else if (id == R.id.nav_modules) { + fragment = ModulesFragment.newInstance(); + } else if (id == R.id.nav_protection) { + fragment = ProtectionFragment.newInstance(); + } + + if (fragment != null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit(); + } + return true; + }); + + // Default to drivers tab + if (savedInstanceState == null) { + nav.setSelectedItemId(R.id.nav_drivers); + } + } + + public DriverRegistry getDriverRegistry() { return driverRegistry; } + public ScopeManager getScopeManager() { return scopeManager; } + public KoManager getKoManager() { return koManager; } + public ProtectionManager getProtectionManager() { return protectionManager; } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/ui/ModulesFragment.java b/app/src/main/java/com/seteclabs/drivermanager/ui/ModulesFragment.java new file mode 100644 index 0000000..79bb6ed --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/ui/ModulesFragment.java @@ -0,0 +1,162 @@ +package com.seteclabs.drivermanager.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.seteclabs.drivermanager.R; +import com.seteclabs.drivermanager.manager.KoManager; +import com.seteclabs.drivermanager.model.KernelModule; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Kernel module (.ko) manager. + * Load/unload modules, set autoload, view module info. + */ +public class ModulesFragment extends Fragment { + + private KoManager koManager; + private RecyclerView recyclerView; + private ModuleAdapter adapter; + private TextView emptyView; + + public static ModulesFragment newInstance() { + return new ModulesFragment(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_modules, container, false); + + koManager = ((MainActivity) requireActivity()).getKoManager(); + + recyclerView = view.findViewById(R.id.module_list); + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + adapter = new ModuleAdapter(); + recyclerView.setAdapter(adapter); + + emptyView = view.findViewById(R.id.empty_view); + + view.findViewById(R.id.btn_refresh).setOnClickListener(v -> refreshList()); + + refreshList(); + return view; + } + + private void refreshList() { + new Thread(() -> { + List modules = koManager.listModules(); + requireActivity().runOnUiThread(() -> { + adapter.setModules(modules); + emptyView.setVisibility(modules.isEmpty() ? View.VISIBLE : View.GONE); + recyclerView.setVisibility(modules.isEmpty() ? View.GONE : View.VISIBLE); + }); + }).start(); + } + + class ModuleAdapter extends RecyclerView.Adapter { + private List modules = new ArrayList<>(); + + void setModules(List modules) { + this.modules = modules; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_module, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + KernelModule mod = modules.get(position); + + holder.name.setText(mod.getName()); + holder.desc.setText(mod.getDescription() != null ? mod.getDescription() : "No description"); + holder.meta.setText(String.format(Locale.US, "v%s | %s | %s", + mod.getVersion() != null ? mod.getVersion() : "?", + formatSize(mod.getSize()), + mod.isCompatible() ? "Compatible" : "Unknown compat")); + + // Load/unload toggle + holder.loadToggle.setOnCheckedChangeListener(null); + holder.loadToggle.setChecked(mod.isLoaded()); + holder.loadToggle.setOnCheckedChangeListener((v, checked) -> { + new Thread(() -> { + String result; + if (checked) { + result = koManager.loadModule(mod.getFilename(), null); + } else { + result = koManager.unloadModule(mod.getName()); + } + String msg = result; + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show(); + refreshList(); + }); + }).start(); + }); + + // Autoload checkbox + holder.autoload.setOnCheckedChangeListener(null); + holder.autoload.setChecked(mod.isAutoload()); + holder.autoload.setOnCheckedChangeListener((v, checked) -> { + koManager.setAutoload(mod.getFilename(), checked); + }); + + // Click for details + holder.itemView.setOnClickListener(v -> { + new Thread(() -> { + String info = koManager.getModuleInfo(mod.getFilename()); + requireActivity().runOnUiThread(() -> { + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle(mod.getName()) + .setMessage(info) + .setPositiveButton("OK", null) + .show(); + }); + }).start(); + }); + } + + @Override + public int getItemCount() { return modules.size(); } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView name, desc, meta; + SwitchMaterial loadToggle; + CheckBox autoload; + + ViewHolder(View v) { + super(v); + name = v.findViewById(R.id.module_name); + desc = v.findViewById(R.id.module_desc); + meta = v.findViewById(R.id.module_meta); + loadToggle = v.findViewById(R.id.module_toggle); + autoload = v.findViewById(R.id.module_autoload); + } + } + } + + private String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024) + " KB"; + return String.format(Locale.US, "%.1f MB", bytes / (1024.0 * 1024.0)); + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/ui/ProtectionFragment.java b/app/src/main/java/com/seteclabs/drivermanager/ui/ProtectionFragment.java new file mode 100644 index 0000000..e571e9d --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/ui/ProtectionFragment.java @@ -0,0 +1,139 @@ +package com.seteclabs.drivermanager.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.seteclabs.drivermanager.R; +import com.seteclabs.drivermanager.manager.ProtectionManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Driver integrity protection view. + * Shows baseline status, check results, and allows creating new baselines. + */ +public class ProtectionFragment extends Fragment { + + private ProtectionManager protectionManager; + private TextView statusText, statsText; + private RecyclerView resultList; + private ResultAdapter adapter; + + public static ProtectionFragment newInstance() { + return new ProtectionFragment(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_protection, container, false); + + protectionManager = ((MainActivity) requireActivity()).getProtectionManager(); + + statusText = view.findViewById(R.id.protection_status); + statsText = view.findViewById(R.id.protection_stats); + + resultList = view.findViewById(R.id.result_list); + resultList.setLayoutManager(new LinearLayoutManager(requireContext())); + adapter = new ResultAdapter(); + resultList.setAdapter(adapter); + + Button btnBaseline = view.findViewById(R.id.btn_baseline); + btnBaseline.setOnClickListener(v -> { + new Thread(() -> { + int count = protectionManager.createBaseline(); + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), "Baseline created: " + count + " drivers", Toast.LENGTH_SHORT).show(); + refreshStatus(); + }); + }).start(); + }); + + Button btnCheck = view.findViewById(R.id.btn_check); + btnCheck.setOnClickListener(v -> { + new Thread(() -> { + List results = protectionManager.checkIntegrity(); + requireActivity().runOnUiThread(() -> { + adapter.setResults(results); + refreshStatus(); + int changes = 0; + for (ProtectionManager.CheckResult r : results) { + if (!"ok".equals(r.status)) changes++; + } + if (changes > 0) { + Toast.makeText(requireContext(), "WARNING: " + changes + " driver(s) changed!", Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(requireContext(), "All drivers OK", Toast.LENGTH_SHORT).show(); + } + }); + }).start(); + }); + + refreshStatus(); + return view; + } + + private void refreshStatus() { + new Thread(() -> { + ProtectionManager.Stats stats = protectionManager.getStats(); + requireActivity().runOnUiThread(() -> { + if (stats.totalProtected == 0) { + statusText.setText("No baseline created"); + statsText.setText("Tap 'Create Baseline' to start protecting drivers"); + } else { + statusText.setText(stats.totalProtected + " drivers protected"); + statsText.setText("OK: " + stats.ok + " | Changed: " + stats.changed + " | Missing: " + stats.missing); + } + }); + }).start(); + } + + class ResultAdapter extends RecyclerView.Adapter { + private List results = new ArrayList<>(); + + void setResults(List results) { + this.results = results; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_2, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ProtectionManager.CheckResult result = results.get(position); + holder.path.setText(result.path); + holder.status.setText(result.status.toUpperCase()); + holder.status.setTextColor( + "ok".equals(result.status) ? 0xFF2ECC71 : + "changed".equals(result.status) ? 0xFFE94560 : 0xFFF39C12); + } + + @Override + public int getItemCount() { return results.size(); } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView path, status; + ViewHolder(View v) { + super(v); + path = v.findViewById(android.R.id.text1); + status = v.findViewById(android.R.id.text2); + } + } + } +} diff --git a/app/src/main/java/com/seteclabs/drivermanager/xposed/DriverHook.java b/app/src/main/java/com/seteclabs/drivermanager/xposed/DriverHook.java new file mode 100644 index 0000000..abe98f2 --- /dev/null +++ b/app/src/main/java/com/seteclabs/drivermanager/xposed/DriverHook.java @@ -0,0 +1,319 @@ +package com.seteclabs.drivermanager.xposed; + +import android.app.Application; +import android.content.Context; +import android.os.Environment; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.File; +import java.io.FileReader; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.IXposedHookZygoteInit; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * Xposed hook entry point for Driver Manager. + * + * Intercepts native library loading (System.loadLibrary / System.load / dlopen) + * and redirects to custom driver files for scoped apps. + * + * Stock system drivers are NEVER modified on disk. + * Custom drivers are loaded in-process only for apps the user has selected. + */ +public class DriverHook implements IXposedHookLoadPackage, IXposedHookZygoteInit { + + private static final String TAG = "DriverManager"; + private static final String PACKAGE = "com.seteclabs.drivermanager"; + private static final String CONFIG_DIR = "/data/local/tmp/driver-manager"; + private static final String DRIVERS_DIR = "/data/local/tmp/driver-manager/drivers"; + + // Map of library name -> custom path for the current package + private Map redirectMap = new HashMap<>(); + + // Whether this package is scoped at all + private boolean isScoped = false; + + @Override + public void initZygote(StartupParam startupParam) { + // Ensure config directory exists (created by the app with root) + XposedBridge.log(TAG + ": Zygote initialized"); + } + + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) { + // Skip our own package + if (PACKAGE.equals(lpparam.packageName)) return; + + // Load redirect configuration for this package + loadConfig(lpparam.packageName); + if (!isScoped || redirectMap.isEmpty()) return; + + XposedBridge.log(TAG + ": Hooking " + lpparam.packageName + + " with " + redirectMap.size() + " driver redirects"); + + // Hook 1: System.loadLibrary(String libName) + // This is the most common way apps load native libraries + XposedHelpers.findAndHookMethod( + "java.lang.System", + lpparam.classLoader, + "loadLibrary", + String.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String libName = (String) param.args[0]; + String customPath = redirectMap.get(libName); + + if (customPath != null) { + File customFile = new File(customPath); + if (customFile.exists() && customFile.canRead()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] Redirecting lib '" + libName + "' -> " + customPath); + try { + System.load(customPath); + param.setResult(null); // Skip original loadLibrary + } catch (UnsatisfiedLinkError e) { + XposedBridge.log(TAG + ": Failed to load custom driver: " + e.getMessage()); + // Fall through to original + } + } else { + XposedBridge.log(TAG + ": Custom driver missing: " + customPath); + } + } + } + } + ); + + // Hook 2: System.load(String absolutePath) + // For apps that load libraries by absolute path + XposedHelpers.findAndHookMethod( + "java.lang.System", + lpparam.classLoader, + "load", + String.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String origPath = (String) param.args[0]; + if (origPath == null) return; + + // Check if the filename matches any redirect + String filename = new File(origPath).getName(); + // Strip lib prefix and .so suffix to get the lib name + String libName = filename; + if (libName.startsWith("lib")) libName = libName.substring(3); + if (libName.endsWith(".so")) libName = libName.substring(0, libName.length() - 3); + + String customPath = redirectMap.get(libName); + // Also check by full filename + if (customPath == null) customPath = redirectMap.get(filename); + // Also check by original absolute path + if (customPath == null) customPath = redirectMap.get(origPath); + + if (customPath != null && new File(customPath).exists()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] Redirecting path '" + origPath + "' -> " + customPath); + param.args[0] = customPath; + } + } + } + ); + + // Hook 3: Runtime.loadLibrary0(ClassLoader, Class, String) + // Internal method that both loadLibrary and load funnel through on Android 10+ + try { + Method loadLib0 = XposedHelpers.findMethodExact( + Runtime.class, "loadLibrary0", + ClassLoader.class, Class.class, String.class); + + XposedBridge.hookMethod(loadLib0, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String libName = (String) param.args[2]; + if (libName == null) return; + + // loadLibrary0 receives the library name (not "lib" prefix or ".so" suffix) + String customPath = redirectMap.get(libName); + + // Also check if it's an absolute path + if (customPath == null && libName.startsWith("/")) { + String filename = new File(libName).getName(); + String baseName = filename; + if (baseName.startsWith("lib")) baseName = baseName.substring(3); + if (baseName.endsWith(".so")) baseName = baseName.substring(0, baseName.length() - 3); + customPath = redirectMap.get(baseName); + if (customPath == null) customPath = redirectMap.get(filename); + } + + if (customPath != null && new File(customPath).exists()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] loadLibrary0 redirect: '" + libName + "' -> " + customPath); + // Replace with absolute path to custom driver + param.args[2] = customPath; + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + ": Could not hook Runtime.loadLibrary0: " + t.getMessage()); + } + + // Hook 4: BaseDexClassLoader.findLibrary(String) + // Returns the full path where a library would be loaded from + // We redirect this to our custom path so the entire loading chain uses it + try { + XposedHelpers.findAndHookMethod( + "dalvik.system.BaseDexClassLoader", + lpparam.classLoader, + "findLibrary", + String.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String libName = (String) param.args[0]; + String customPath = redirectMap.get(libName); + + if (customPath != null && new File(customPath).exists()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] findLibrary redirect: '" + libName + "' -> " + customPath); + param.setResult(customPath); + } + } + } + ); + } catch (Throwable t) { + XposedBridge.log(TAG + ": Could not hook findLibrary: " + t.getMessage()); + } + + // Hook 5: Intercept file open for firmware/config file redirection + // This catches apps that read driver configs or firmware blobs directly + hookFileRedirects(lpparam); + } + + /** + * Hook file open operations to redirect firmware/config file reads. + * This handles cases where apps read driver-adjacent files (configs, firmware blobs) + * that aren't loaded via loadLibrary. + */ + private void hookFileRedirects(XC_LoadPackage.LoadPackageParam lpparam) { + // Check if we have file redirects configured + Map fileRedirects = loadFileRedirects(lpparam.packageName); + if (fileRedirects == null || fileRedirects.isEmpty()) return; + + // Hook FileInputStream constructor to redirect file paths + XposedHelpers.findAndHookConstructor( + "java.io.FileInputStream", + lpparam.classLoader, + File.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + File origFile = (File) param.args[0]; + if (origFile == null) return; + + String origPath = origFile.getAbsolutePath(); + String customPath = fileRedirects.get(origPath); + + if (customPath != null && new File(customPath).exists()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] File redirect: " + origPath + " -> " + customPath); + param.args[0] = new File(customPath); + } + } + } + ); + + // Also hook the String constructor variant + XposedHelpers.findAndHookConstructor( + "java.io.FileInputStream", + lpparam.classLoader, + String.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String origPath = (String) param.args[0]; + if (origPath == null) return; + + String customPath = fileRedirects.get(origPath); + if (customPath != null && new File(customPath).exists()) { + XposedBridge.log(TAG + ": [" + lpparam.packageName + + "] File redirect: " + origPath + " -> " + customPath); + param.args[0] = customPath; + } + } + } + ); + } + + /** + * Load the redirect configuration for a specific package. + * Config is stored as JSON by the Driver Manager app. + * + * Format: /data/local/tmp/driver-manager/scopes/{packageName}.json + * { + * "libraries": { + * "rtlsdr": "/data/local/tmp/driver-manager/drivers/librtlsdr_custom.so", + * "usb-1.0": "/data/local/tmp/driver-manager/drivers/libusb_custom.so" + * }, + * "files": { + * "/vendor/firmware/fw.bin": "/data/local/tmp/driver-manager/drivers/fw_custom.bin" + * } + * } + */ + private void loadConfig(String packageName) { + redirectMap.clear(); + isScoped = false; + + File configFile = new File(CONFIG_DIR + "/scopes/" + packageName + ".json"); + if (!configFile.exists()) return; + + try (FileReader reader = new FileReader(configFile)) { + Gson gson = new Gson(); + ScopeConfig config = gson.fromJson(reader, ScopeConfig.class); + + if (config != null && config.libraries != null) { + redirectMap.putAll(config.libraries); + isScoped = true; + } + } catch (Exception e) { + XposedBridge.log(TAG + ": Error loading config for " + packageName + ": " + e.getMessage()); + } + } + + /** + * Load file redirect map for a package (firmware, config files). + */ + private Map loadFileRedirects(String packageName) { + File configFile = new File(CONFIG_DIR + "/scopes/" + packageName + ".json"); + if (!configFile.exists()) return null; + + try (FileReader reader = new FileReader(configFile)) { + Gson gson = new Gson(); + ScopeConfig config = gson.fromJson(reader, ScopeConfig.class); + return config != null ? config.files : null; + } catch (Exception e) { + return null; + } + } + + /** + * Scope configuration structure. + */ + static class ScopeConfig { + /** Map of library name -> custom .so path */ + Map libraries; + /** Map of original file path -> custom file path */ + Map files; + } +} diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..1aba19f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9cdda61 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_apps.xml b/app/src/main/res/layout/fragment_apps.xml new file mode 100644 index 0000000..734ee76 --- /dev/null +++ b/app/src/main/res/layout/fragment_apps.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_drivers.xml b/app/src/main/res/layout/fragment_drivers.xml new file mode 100644 index 0000000..a9f3297 --- /dev/null +++ b/app/src/main/res/layout/fragment_drivers.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_modules.xml b/app/src/main/res/layout/fragment_modules.xml new file mode 100644 index 0000000..1682261 --- /dev/null +++ b/app/src/main/res/layout/fragment_modules.xml @@ -0,0 +1,50 @@ + + + + + + + +