Driver Manager v2.0.0 - LSPosed module for per-app driver management
This commit is contained in:
41
app/build.gradle.kts
Normal file
41
app/build.gradle.kts
Normal 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")
|
||||
}
|
||||
46
app/src/main/AndroidManifest.xml
Normal file
46
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
1
app/src/main/assets/xposed_init
Normal file
1
app/src/main/assets/xposed_init
Normal file
@@ -0,0 +1 @@
|
||||
com.seteclabs.drivermanager.xposed.DriverHook
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
15
app/src/main/res/drawable/ic_launcher.xml
Normal file
15
app/src/main/res/drawable/ic_launcher.xml
Normal 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>
|
||||
26
app/src/main/res/layout/activity_main.xml
Normal file
26
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
40
app/src/main/res/layout/fragment_apps.xml
Normal file
40
app/src/main/res/layout/fragment_apps.xml
Normal 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>
|
||||
44
app/src/main/res/layout/fragment_drivers.xml
Normal file
44
app/src/main/res/layout/fragment_drivers.xml
Normal 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>
|
||||
50
app/src/main/res/layout/fragment_modules.xml
Normal file
50
app/src/main/res/layout/fragment_modules.xml
Normal 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>
|
||||
86
app/src/main/res/layout/fragment_protection.xml
Normal file
86
app/src/main/res/layout/fragment_protection.xml
Normal 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>
|
||||
66
app/src/main/res/layout/item_app.xml
Normal file
66
app/src/main/res/layout/item_app.xml
Normal 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>
|
||||
72
app/src/main/res/layout/item_driver.xml
Normal file
72
app/src/main/res/layout/item_driver.xml
Normal 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>
|
||||
71
app/src/main/res/layout/item_module.xml
Normal file
71
app/src/main/res/layout/item_module.xml
Normal 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>
|
||||
19
app/src/main/res/menu/bottom_nav.xml
Normal file
19
app/src/main/res/menu/bottom_nav.xml
Normal 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>
|
||||
14
app/src/main/res/values/colors.xml
Normal file
14
app/src/main/res/values/colors.xml
Normal 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>
|
||||
5
app/src/main/res/values/nav_colors.xml
Normal file
5
app/src/main/res/values/nav_colors.xml
Normal 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>
|
||||
12
app/src/main/res/values/strings.xml
Normal file
12
app/src/main/res/values/strings.xml
Normal 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>
|
||||
17
app/src/main/res/values/themes.xml
Normal file
17
app/src/main/res/values/themes.xml
Normal 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>
|
||||
5
app/src/main/res/xml/nav_item_color.xml
Normal file
5
app/src/main/res/xml/nav_item_color.xml
Normal 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>
|
||||
Reference in New Issue
Block a user