Driver Manager v2.0.0 - LSPosed module for per-app driver management

This commit is contained in:
sssnake
2026-04-03 06:38:12 -07:00
commit 3d87e492b2
39 changed files with 2869 additions and 0 deletions

41
app/build.gradle.kts Normal file
View File

@@ -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")
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<!-- LSPosed module metadata -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Per-app driver manager: redirect native library loading to custom drivers without modifying system files" />
<meta-data
android:name="xposedminversion"
android:value="93" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope" />
<!-- Main Activity -->
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Config provider for sharing prefs with hook process -->
<provider
android:name=".manager.ConfigProvider"
android:authorities="com.seteclabs.drivermanager.config"
android:exported="false"
android:grantUriPermissions="true" />
</application>
</manifest>

View File

@@ -0,0 +1 @@
com.seteclabs.drivermanager.xposed.DriverHook

View File

@@ -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; }
}

View File

@@ -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<Driver> 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<List<Driver>>(){}.getType();
List<Driver> 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<Driver> getDrivers() { return drivers; }
public List<Driver> getDriversByCategory(String category) {
List<Driver> result = new ArrayList<>();
for (Driver d : drivers) {
if (d.getCategory().equals(category)) result.add(d);
}
return result;
}
public List<Driver> getEnabledDrivers() {
List<Driver> 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";
}
}

View File

@@ -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<KernelModule> listModules() {
List<KernelModule> 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<String> 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<String> 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<KernelModule> 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");
}
}

View File

@@ -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<String, String> 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<CheckResult> checkIntegrity() {
List<CheckResult> results = new ArrayList<>();
for (Map.Entry<String, String> 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<CheckResult> 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<Map<String, String>>(){}.getType();
Map<String, String> 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;
}
}

View File

@@ -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");
}
}

View File

@@ -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<String, Set<String>> 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<Map<String, Set<String>>>(){}.getType();
Map<String, Set<String>> 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<String> 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<String> apps = scopeMap.get(driverId);
return apps != null && apps.contains(packageName);
}
/**
* Get all packages scoped for a driver.
*/
public Set<String> getScopedApps(String driverId) {
return scopeMap.getOrDefault(driverId, new HashSet<>());
}
/**
* Get all drivers scoped for a package.
*/
public List<String> getScopedDrivers(String packageName) {
List<String> result = new ArrayList<>();
for (Map.Entry<String, Set<String>> 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<String> 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<String> 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<String, Set<String>> 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<String> allPackages = new HashSet<>();
for (Set<String> 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<AppInfo> getInstalledApps() {
List<AppInfo> 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;
}
}

View File

@@ -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<Variant> 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<Variant> getVariants() { return variants; }
public void setVariants(List<Variant> 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; }
}
}

View File

@@ -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; }
}

View File

@@ -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<String, String> libraries = new HashMap<>();
/** Map of original file path -> custom file path */
private Map<String, String> files = new HashMap<>();
public Scope() {}
public Map<String, String> getLibraries() { return libraries; }
public void setLibraries(Map<String, String> libraries) { this.libraries = libraries; }
public Map<String, String> getFiles() { return files; }
public void setFiles(Map<String, String> 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();
}
}

View File

@@ -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<Driver> enabledDrivers = new ArrayList<>();
private List<ScopeManager.AppInfo> allApps = new ArrayList<>();
private List<ScopeManager.AppInfo> 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<String> driverNames = new ArrayList<>();
driverNames.add("Select a driver to scope...");
for (Driver d : enabledDrivers) {
driverNames.add("[" + d.getCategory().toUpperCase() + "] " + d.getName());
}
ArrayAdapter<String> 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<AppAdapter.ViewHolder> {
@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);
}
}
}
}

View File

@@ -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<Driver> 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<DriverAdapter.ViewHolder> {
private List<Driver> drivers = new ArrayList<>();
void setDrivers(List<Driver> 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();
}
}

View File

@@ -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; }
}

View File

@@ -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<KernelModule> 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<ModuleAdapter.ViewHolder> {
private List<KernelModule> modules = new ArrayList<>();
void setModules(List<KernelModule> 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));
}
}

View File

@@ -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<ProtectionManager.CheckResult> 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<ResultAdapter.ViewHolder> {
private List<ProtectionManager.CheckResult> results = new ArrayList<>();
void setResults(List<ProtectionManager.CheckResult> 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);
}
}
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> libraries;
/** Map of original file path -> custom file path */
Map<String, String> files;
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background circle -->
<path
android:fillColor="#4F6DF5"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" />
<!-- DM letters -->
<path
android:fillColor="#FFFFFF"
android:pathData="M12,14h4l4,8 4,-8h4v20h-4v-12l-4,8 -4,-8v12h-4z" />
</vector>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="56dp" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:background="@color/surface"
app:itemIconTint="@color/nav_item_color"
app:itemTextColor="@color/nav_item_color"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Driver selector -->
<Spinner
android:id="@+id/driver_spinner"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="12dp"
android:background="@color/surface2"
android:padding="8dp"
android:popupBackground="@color/surface" />
<!-- Search bar -->
<EditText
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_marginHorizontal="12dp"
android:layout_marginBottom="8dp"
android:background="@color/surface2"
android:hint="Search apps..."
android:textColorHint="@color/text_dim"
android:textColor="@color/text"
android:paddingHorizontal="12dp"
android:singleLine="true"
android:inputType="text" />
<!-- App list -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<com.google.android.material.chip.ChipGroup
android:id="@+id/category_chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleSelection="true" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/driver_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="Scan drivers"
android:src="@android:drawable/ic_menu_search"
app:backgroundTint="@color/accent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Kernel Modules (.ko)"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold" />
<Button
android:id="@+id/btn_refresh"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="Refresh"
android:textSize="12sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="40dp"
android:text="No .ko modules found\n\nPlace .ko files in:\n/data/local/tmp/driver-manager/modules/"
android:textColor="@color/text_dim"
android:textAlignment="center"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- Status card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/protection_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="No baseline"
android:textColor="@color/text"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/protection_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="--"
android:textColor="@color/text_dim"
android:textSize="13sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_baseline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:text="Create Baseline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<Button
android:id="@+id/btn_check"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="6dp"
android:text="Check Integrity" />
</LinearLayout>
<!-- Results list -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Check Results"
android:textColor="@color/text"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/result_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="10dp"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/app_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text"
android:textSize="14sp" />
<TextView
android:id="@+id/system_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SYSTEM"
android:textColor="@color/warning"
android:textSize="9sp"
android:textStyle="bold"
android:layout_marginStart="8dp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/app_package"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_dim"
android:textSize="11sp"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout>
<CheckBox
android:id="@+id/scope_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:buttonTint="@color/accent" />
</LinearLayout>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="14dp"
android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/driver_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/text"
android:textSize="14sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="end" />
<TextView
android:id="@+id/driver_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/accent"
android:textSize="10sp"
android:textStyle="bold"
android:background="@color/surface2"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:id="@+id/driver_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_dim"
android:textSize="11sp"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/driver_variants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_dim"
android:textSize="11sp"
android:layout_marginTop="2dp" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/driver_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp" />
</LinearLayout>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="14dp"
android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/module_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/module_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_dim"
android:textSize="12sp"
android:layout_marginTop="2dp" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/module_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="6dp"
android:paddingStart="0dp">
<TextView
android:id="@+id/module_meta"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/text_dim"
android:textSize="11sp" />
<CheckBox
android:id="@+id/module_autoload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Autoload"
android:textColor="@color/text_dim"
android:textSize="11sp"
android:buttonTint="@color/accent" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_drivers"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/nav_drivers" />
<item
android:id="@+id/nav_apps"
android:icon="@android:drawable/ic_menu_more"
android:title="@string/nav_apps" />
<item
android:id="@+id/nav_modules"
android:icon="@android:drawable/ic_menu_set_as"
android:title="@string/nav_modules" />
<item
android:id="@+id/nav_protection"
android:icon="@android:drawable/ic_lock_lock"
android:title="@string/nav_protection" />
</menu>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="bg">#FF0F0F1A</color>
<color name="surface">#FF1A1A2E</color>
<color name="surface2">#FF16213E</color>
<color name="accent">#FF4F6DF5</color>
<color name="accent_dim">#FF3A52B5</color>
<color name="danger">#FFE94560</color>
<color name="success">#FF2ECC71</color>
<color name="warning">#FFF39C12</color>
<color name="text">#FFE8E8F0</color>
<color name="text_dim">#FF8888A0</color>
<color name="border">#FF2A2A40</color>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="nav_item_color">#FF8888A0</color>
<color name="nav_item_selected">#FF4F6DF5</color>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Driver Manager</string>
<string name="nav_drivers">Drivers</string>
<string name="nav_apps">Apps</string>
<string name="nav_modules">Modules</string>
<string name="nav_protection">Protect</string>
<string-array name="xposed_scope">
<!-- Empty = hook all packages (LSPosed will let user select scope) -->
</string-array>
</resources>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/accent</item>
<item name="colorPrimaryDark">@color/bg</item>
<item name="colorAccent">@color/accent</item>
<item name="android:windowBackground">@color/bg</item>
<item name="android:statusBarColor">@color/bg</item>
<item name="android:navigationBarColor">@color/surface</item>
<item name="android:textColor">@color/text</item>
<item name="android:textColorHint">@color/text_dim</item>
</style>
<style name="nav_item_color" parent="">
<item name="android:color">@color/text_dim</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/accent" />
<item android:color="@color/text_dim" />
</selector>