Driver Manager v2.0.0 - LSPosed module for per-app driver management
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.claude/
|
||||||
|
.claude*
|
||||||
|
*.apk
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
local.properties
|
||||||
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>
|
||||||
3
build.gradle.kts
Normal file
3
build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.7.3" apply false
|
||||||
|
}
|
||||||
0
drivers/.gitkeep
Normal file
0
drivers/.gitkeep
Normal file
3
gradle.properties
Normal file
3
gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
17
settings.gradle.kts
Normal file
17
settings.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolution {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://api.xposed.info/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "DriverManager"
|
||||||
|
include(":app")
|
||||||
Reference in New Issue
Block a user