009: publish AXPR23 final source and public documentation
This commit is contained in:
1 parent
72930e0fd6
commit
2c714d8d34
35 files changed
+2498
-403
No files matched your search
@@ -0,0 +1,62 @@
|
||||
# IntelliRAT v0.2
|
||||
|
||||
Systemless Magisk controller for automatic, reversible Android 16
|
||||
network-profile changes. It does not modify `/system`, `/system_ext`, `/vendor`,
|
||||
RIL, modem firmware or framework.
|
||||
|
||||
## Scope
|
||||
|
||||
- Backend: Android 16 `cmd phone set-allowed-network-types-for-users`.
|
||||
- Target: one slot at a time, default slot `0`.
|
||||
- HIGH: LTE + UMTS/HSPA + GSM families.
|
||||
- LOW: UMTS/HSPA + GSM families.
|
||||
- Safety: capture/restore, active-call refusal, result verification and event log.
|
||||
- No Xposed or Vector. The companion APK source is included under `app/`; the
|
||||
module zip includes it only after the AOSP app build produces
|
||||
`system/app/IntelliRAT/IntelliRAT.apk`.
|
||||
|
||||
The binary masks are derived from the LOS23/A16 `TelephonyManager` constants:
|
||||
network type bits are `1 << (NETWORK_TYPE - 1)`, with LTE CA normalized to LTE.
|
||||
They must be treated as target-build data, not a universal Qualcomm constant.
|
||||
|
||||
## Install on GOLDEN only
|
||||
|
||||
Do not install this in the production ROM or on LAB yet. Build a Magisk module
|
||||
zip with `scripts/build-intellirat-module.sh`, install it on GOLDEN with a test SIM, and keep the
|
||||
known-good ROM available. GOLDEN now detects the inserted test SIM as
|
||||
`LOADED` on `SIMon mobile`.
|
||||
|
||||
After Magisk loads the module:
|
||||
|
||||
```text
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat status 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat capture 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat --dry-run high 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat high 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat status 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat low 0
|
||||
su -g 2000 -G 2000 -c /system/bin/intellirat restore 0
|
||||
```
|
||||
|
||||
`capture` must be run before a transition. `restore` uses the captured user
|
||||
mask and does not guess a stock mode. `events.log` is stored under
|
||||
`/data/local/tmp/intellirat/`. The prototype uses the root-owned `shell` group
|
||||
for this small network-mask state because MagiskSU drops DAC capabilities for
|
||||
`su` commands; no credentials or private data are stored.
|
||||
|
||||
When the companion APK is present, launch **IntelliRAT** from the app drawer.
|
||||
It shows the current backend result, slot selector, automatic toggle, delay,
|
||||
foreground-package exceptions, capture/HIGH/LOW/restore buttons and a
|
||||
persistent status notification with quick actions. The automatic policy uses
|
||||
lock delay, unlock restoration and a safe call guard.
|
||||
|
||||
## Not implemented yet
|
||||
|
||||
- traffic protection
|
||||
- multi-SIM/eSIM UI and subscription discovery
|
||||
- Quick Settings tile
|
||||
|
||||
The current automatic policy is deliberately limited to Keyguard lock state, a
|
||||
configurable delay and explicit foreground-package exceptions. Wi-Fi, battery,
|
||||
traffic and multi-SIM rules remain disabled until their real device behavior is
|
||||
specified and tested with calls, IMS, SMS, data and signal loss.
|
||||
@@ -0,0 +1,10 @@
|
||||
android_app {
|
||||
name: "IntelliRAT",
|
||||
srcs: ["src/**/*.java"],
|
||||
manifest: "AndroidManifest.xml",
|
||||
certificate: "platform",
|
||||
platform_apis: true,
|
||||
optimize: {
|
||||
proguard_flags_files: ["proguard.flags"],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.axp.intellirat">
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="IntelliRAT"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".IntelliRatService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="telephony network profile controller" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".ActionReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:directBootAware="false"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.USER_UNLOCKED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".IntelliRatTileService"
|
||||
android:label="IntelliRAT HIGH/LOW"
|
||||
android:icon="@drawable/ic_intellirat"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1 @@
|
||||
-keep class com.axp.intellirat.** { *; }
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M3,5h18v14H3zM5,7v10h14V7zM7,9h2v2H7zM11,9h6v2h-6zM7,13h2v2H7zM11,13h6v2h-6z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public final class ActionReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String command = intent.getStringExtra(IntelliRatService.EXTRA_COMMAND);
|
||||
if (command == null) return;
|
||||
Intent service = new Intent(context, IntelliRatService.class);
|
||||
service.putExtra(IntelliRatService.EXTRA_COMMAND, command);
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
final class Backend {
|
||||
static final class Result {
|
||||
final int exitCode;
|
||||
final String output;
|
||||
|
||||
Result(int exitCode, String output) {
|
||||
this.exitCode = exitCode;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
boolean ok() {
|
||||
return exitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Backend() {}
|
||||
|
||||
static Result run(String command, String slot) {
|
||||
if (!slot.matches("[0-9]+")) {
|
||||
return new Result(2, "Invalid slot: " + slot);
|
||||
}
|
||||
String shellCommand = "/system/bin/intellirat " + command + " " + slot;
|
||||
Process process = null;
|
||||
try {
|
||||
process = new ProcessBuilder("su", "-g", "2000", "-G", "2000", "-c", shellCommand)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
StringBuilder output = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (output.length() > 0) output.append('\n');
|
||||
output.append(line);
|
||||
}
|
||||
}
|
||||
return new Result(process.waitFor(), output.toString());
|
||||
} catch (IOException e) {
|
||||
return new Result(127, "Root backend unavailable: " + e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return new Result(130, "Interrupted");
|
||||
} finally {
|
||||
if (process != null) process.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
public final class BootReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!context.getSharedPreferences(IntelliRatService.PREFS, Context.MODE_PRIVATE)
|
||||
.getBoolean(IntelliRatService.AUTO_ENABLED, false)) {
|
||||
return;
|
||||
}
|
||||
Intent service = new Intent(context, IntelliRatService.class);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(service);
|
||||
} else {
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.KeyguardManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Build;
|
||||
import android.app.usage.UsageEvents;
|
||||
import android.app.usage.UsageStatsManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkCapabilities;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class IntelliRatService extends Service {
|
||||
static final String ACTION_COMMAND = "com.axp.intellirat.COMMAND";
|
||||
static final String EXTRA_COMMAND = "command";
|
||||
static final String ACTION_AUTO_CONFIG = "com.axp.intellirat.AUTO_CONFIG";
|
||||
static final String EXTRA_AUTO_ENABLED = "automatic_enabled";
|
||||
static final String EXTRA_AUTO_DELAY_SECONDS = "automatic_delay_seconds";
|
||||
static final String EXTRA_AUTO_UNLOCK_DELAY_SECONDS = "automatic_unlock_delay_seconds";
|
||||
static final String EXTRA_AUTO_EXCEPTIONS = "automatic_exceptions";
|
||||
static final String PREFS = "intellirat";
|
||||
static final String AUTO_ENABLED = "automatic_enabled";
|
||||
static final String AUTO_DELAY_SECONDS = "automatic_delay_seconds";
|
||||
static final String AUTO_UNLOCK_DELAY_SECONDS = "automatic_unlock_delay_seconds";
|
||||
static final String AUTO_EXCEPTIONS = "automatic_exceptions";
|
||||
static final String CHANNEL = "intellirat_status";
|
||||
private static final int NOTIFICATION_ID = 4103;
|
||||
private static final int RETRY_DELAY_SECONDS = 30;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private Runnable lowTask;
|
||||
private Runnable unlockTask;
|
||||
private Runnable foregroundPoll;
|
||||
private Runnable lockPoll;
|
||||
private Runnable dataRecovery;
|
||||
private int dataRecoveryAttempt;
|
||||
private boolean charging;
|
||||
private final BroadcastReceiver chargingReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
boolean nowCharging = isCharging();
|
||||
if (nowCharging != charging) {
|
||||
charging = nowCharging;
|
||||
onChargingChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
private boolean keyguardLocked;
|
||||
private boolean foregroundExceptionActive;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createChannel();
|
||||
startForeground(NOTIFICATION_ID, notification("Starting root backend..."));
|
||||
keyguardLocked = isDeviceKeyguardLocked();
|
||||
charging = isCharging();
|
||||
IntentFilter powerFilter = new IntentFilter();
|
||||
powerFilter.addAction(Intent.ACTION_POWER_CONNECTED);
|
||||
powerFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
|
||||
registerReceiver(chargingReceiver, powerFilter);
|
||||
refresh("status");
|
||||
if (isAutomaticEnabled()) {
|
||||
configureAutomatic(true,
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getInt(AUTO_DELAY_SECONDS, 30),
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getInt(AUTO_UNLOCK_DELAY_SECONDS, 0),
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getString(AUTO_EXCEPTIONS, ""));
|
||||
}
|
||||
scheduleForegroundPoll();
|
||||
scheduleLockPoll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_AUTO_CONFIG.equals(intent.getAction())) {
|
||||
configureAutomatic(
|
||||
intent.getBooleanExtra(EXTRA_AUTO_ENABLED, false),
|
||||
intent.getIntExtra(EXTRA_AUTO_DELAY_SECONDS, 30),
|
||||
intent.getIntExtra(EXTRA_AUTO_UNLOCK_DELAY_SECONDS, 0),
|
||||
intent.getStringExtra(EXTRA_AUTO_EXCEPTIONS));
|
||||
} else if (intent != null && intent.hasExtra(EXTRA_COMMAND)) {
|
||||
enqueueTransition(intent.getStringExtra(EXTRA_COMMAND), "Manual transition");
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
cancelLow();
|
||||
cancelUnlock();
|
||||
cancelForegroundPoll();
|
||||
cancelLockPoll();
|
||||
cancelDataRecovery();
|
||||
unregisterReceiver(chargingReceiver);
|
||||
executor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void configureAutomatic(boolean enabled, int delaySeconds, int unlockDelaySeconds,
|
||||
String exceptions) {
|
||||
if (delaySeconds < 5 || delaySeconds > 3600) {
|
||||
updateNotification("Automatic policy rejected: delay must be 5-3600 seconds");
|
||||
return;
|
||||
}
|
||||
if (unlockDelaySeconds < 0 || unlockDelaySeconds > 3600) {
|
||||
updateNotification("Automatic policy rejected: unlock delay must be 0-3600 seconds");
|
||||
return;
|
||||
}
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putBoolean(AUTO_ENABLED, enabled)
|
||||
.putInt(AUTO_DELAY_SECONDS, delaySeconds)
|
||||
.putInt(AUTO_UNLOCK_DELAY_SECONDS, unlockDelaySeconds)
|
||||
.putString(AUTO_EXCEPTIONS, exceptions == null ? "" : exceptions)
|
||||
.apply();
|
||||
cancelLow();
|
||||
cancelUnlock();
|
||||
if (!enabled) {
|
||||
cancelForegroundPoll();
|
||||
cancelLockPoll();
|
||||
cancelDataRecovery();
|
||||
enqueueTransition("high", "Automatic policy disabled");
|
||||
return;
|
||||
}
|
||||
scheduleLockPoll();
|
||||
scheduleForegroundPoll();
|
||||
executor.execute(() -> {
|
||||
if (charging) {
|
||||
updateNotification("Charging: HIGH/LTE forced");
|
||||
runTransition("high", "Charging forced HIGH");
|
||||
return;
|
||||
}
|
||||
updateNotification("Automatic policy enabled: LOW after lock "
|
||||
+ delaySeconds + "s, HIGH after unlock " + unlockDelaySeconds + "s");
|
||||
keyguardLocked = isDeviceKeyguardLocked();
|
||||
if (!keyguardLocked) {
|
||||
scheduleUnlockHigh();
|
||||
} else if (isForegroundException()) {
|
||||
runTransition("high", "Streaming exception kept HIGH");
|
||||
} else {
|
||||
scheduleLow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleLow() {
|
||||
cancelLow();
|
||||
if (!isAutomaticEnabled() || !keyguardLocked || charging) return;
|
||||
int delay = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getInt(AUTO_DELAY_SECONDS, 30);
|
||||
lowTask = () -> {
|
||||
if (isAutomaticEnabled() && keyguardLocked) {
|
||||
enqueueTransition("low", "Automatic locked LOW");
|
||||
}
|
||||
};
|
||||
handler.postDelayed(lowTask, delay * 1000L);
|
||||
updateNotification("Locked: LOW in " + delay + "s");
|
||||
}
|
||||
|
||||
private void cancelLow() {
|
||||
if (lowTask != null) {
|
||||
handler.removeCallbacks(lowTask);
|
||||
lowTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleUnlockHigh() {
|
||||
cancelUnlock();
|
||||
if (!isAutomaticEnabled() || keyguardLocked || charging) return;
|
||||
int delay = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getInt(AUTO_UNLOCK_DELAY_SECONDS, 0);
|
||||
unlockTask = () -> {
|
||||
if (isAutomaticEnabled() && !keyguardLocked) {
|
||||
enqueueTransition("high", "Automatic unlocked HIGH");
|
||||
}
|
||||
};
|
||||
if (delay == 0) {
|
||||
handler.post(unlockTask);
|
||||
} else {
|
||||
handler.postDelayed(unlockTask, delay * 1000L);
|
||||
updateNotification("Unlocked: HIGH in " + delay + "s");
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelUnlock() {
|
||||
if (unlockTask != null) {
|
||||
handler.removeCallbacks(unlockTask);
|
||||
unlockTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleLockPoll() {
|
||||
cancelLockPoll();
|
||||
if (!isAutomaticEnabled()) return;
|
||||
lockPoll = () -> {
|
||||
if (!isAutomaticEnabled()) return;
|
||||
boolean locked = isDeviceKeyguardLocked();
|
||||
if (locked != keyguardLocked) {
|
||||
keyguardLocked = locked;
|
||||
cancelLow();
|
||||
if (locked) {
|
||||
if (foregroundExceptionActive) {
|
||||
enqueueTransition("high", "Streaming exception kept HIGH");
|
||||
} else {
|
||||
scheduleLow();
|
||||
}
|
||||
} else {
|
||||
scheduleUnlockHigh();
|
||||
}
|
||||
}
|
||||
handler.postDelayed(lockPoll, 1000L);
|
||||
};
|
||||
handler.post(lockPoll);
|
||||
}
|
||||
|
||||
private void cancelLockPoll() {
|
||||
if (lockPoll != null) {
|
||||
handler.removeCallbacks(lockPoll);
|
||||
lockPoll = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleDataRecovery() {
|
||||
cancelDataRecovery();
|
||||
dataRecoveryAttempt = 0;
|
||||
dataRecovery = () -> {
|
||||
if (!isAutomaticEnabled() || !keyguardLocked || hasValidatedData()
|
||||
|| dataRecoveryAttempt >= 3) {
|
||||
return;
|
||||
}
|
||||
int attempt = ++dataRecoveryAttempt;
|
||||
executor.execute(() -> {
|
||||
Backend.Result result = Backend.run("low", "0");
|
||||
updateNotification(result.ok()
|
||||
? "LOW data recovery attempt " + attempt + ": " + result.output
|
||||
: "LOW data recovery failed: " + result.output);
|
||||
handler.postDelayed(dataRecovery, attempt * 15000L);
|
||||
});
|
||||
};
|
||||
handler.postDelayed(dataRecovery, 10000L);
|
||||
}
|
||||
|
||||
private void cancelDataRecovery() {
|
||||
if (dataRecovery != null) {
|
||||
handler.removeCallbacks(dataRecovery);
|
||||
dataRecovery = null;
|
||||
}
|
||||
dataRecoveryAttempt = 0;
|
||||
}
|
||||
|
||||
private boolean hasValidatedData() {
|
||||
ConnectivityManager connectivity =
|
||||
(ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
|
||||
NetworkCapabilities capabilities = connectivity.getNetworkCapabilities(
|
||||
connectivity.getActiveNetwork());
|
||||
return capabilities != null
|
||||
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
|
||||
}
|
||||
|
||||
private boolean isDeviceKeyguardLocked() {
|
||||
KeyguardManager keyguard = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||
if (keyguard.isKeyguardLocked()) return true;
|
||||
return Build.VERSION.SDK_INT >= 23 && keyguard.isDeviceLocked();
|
||||
}
|
||||
|
||||
private boolean isCharging() {
|
||||
Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
if (battery == null) return false;
|
||||
int status = battery.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
return status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
}
|
||||
|
||||
private void onChargingChanged() {
|
||||
if (!isAutomaticEnabled()) return;
|
||||
cancelLow();
|
||||
cancelUnlock();
|
||||
cancelDataRecovery();
|
||||
if (charging) {
|
||||
enqueueTransition("high", "Charging forced HIGH");
|
||||
updateNotification("Charging: HIGH/LTE forced");
|
||||
} else if (keyguardLocked) {
|
||||
scheduleLow();
|
||||
} else {
|
||||
scheduleUnlockHigh();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleForegroundPoll() {
|
||||
cancelForegroundPoll();
|
||||
if (!isAutomaticEnabled()) return;
|
||||
foregroundPoll = () -> {
|
||||
if (!isAutomaticEnabled()) return;
|
||||
if (!keyguardLocked) {
|
||||
boolean exception = isForegroundException();
|
||||
if (exception != foregroundExceptionActive) {
|
||||
foregroundExceptionActive = exception;
|
||||
if (exception) {
|
||||
enqueueTransition("high", "Foreground exception kept HIGH");
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.postDelayed(foregroundPoll, 5000L);
|
||||
};
|
||||
handler.post(foregroundPoll);
|
||||
}
|
||||
|
||||
private void cancelForegroundPoll() {
|
||||
if (foregroundPoll != null) {
|
||||
handler.removeCallbacks(foregroundPoll);
|
||||
foregroundPoll = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isForegroundException() {
|
||||
String configured = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getString(AUTO_EXCEPTIONS, "");
|
||||
if (configured.trim().isEmpty()) return false;
|
||||
String packageName = null;
|
||||
try {
|
||||
UsageStatsManager usage = (UsageStatsManager) getSystemService(USAGE_STATS_SERVICE);
|
||||
UsageEvents events = usage.queryEvents(
|
||||
System.currentTimeMillis() - 30000L, System.currentTimeMillis());
|
||||
UsageEvents.Event event = new UsageEvents.Event();
|
||||
while (events != null && events.hasNextEvent()) {
|
||||
events.getNextEvent(event);
|
||||
if (event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND) {
|
||||
packageName = event.getPackageName();
|
||||
}
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
return false;
|
||||
}
|
||||
if (packageName == null) return foregroundExceptionActive;
|
||||
for (String candidate : configured.split("[,\\n\\r ]+")) {
|
||||
if (candidate.trim().equals(packageName)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void enqueueTransition(String command, String label) {
|
||||
executor.execute(() -> runTransition(command, label));
|
||||
}
|
||||
|
||||
private void runTransition(String command, String label) {
|
||||
Backend.Result result = Backend.run(command, "0");
|
||||
String text = result.ok() ? label + ": " + result.output
|
||||
: label + " failed: " + result.output;
|
||||
updateNotification(text);
|
||||
if (result.ok() && "low".equals(command) && isAutomaticEnabled() && keyguardLocked) {
|
||||
scheduleDataRecovery();
|
||||
} else if ("high".equals(command)) {
|
||||
cancelDataRecovery();
|
||||
}
|
||||
if (!result.ok() && "low".equals(command) && isAutomaticEnabled() && keyguardLocked) {
|
||||
handler.postDelayed(() -> {
|
||||
if (isAutomaticEnabled() && keyguardLocked) {
|
||||
enqueueTransition("low", "Automatic locked LOW retry");
|
||||
}
|
||||
}, RETRY_DELAY_SECONDS * 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh(String command) {
|
||||
executor.execute(() -> {
|
||||
Backend.Result result = Backend.run(command, "0");
|
||||
String text = result.ok() ? result.output : "Backend error: " + result.output;
|
||||
updateNotification(text);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateNotification(String text) {
|
||||
((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
|
||||
.notify(NOTIFICATION_ID, notification(text));
|
||||
}
|
||||
|
||||
private boolean isAutomaticEnabled() {
|
||||
return getSharedPreferences(PREFS, MODE_PRIVATE).getBoolean(AUTO_ENABLED, false);
|
||||
}
|
||||
|
||||
private Notification notification(String text) {
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
PendingIntent content = PendingIntent.getActivity(
|
||||
this, 1, open, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new Notification.Builder(this, CHANNEL)
|
||||
.setSmallIcon(android.R.drawable.ic_menu_manage)
|
||||
.setContentTitle("IntelliRAT")
|
||||
.setContentText(oneLine(text))
|
||||
.setStyle(new Notification.BigTextStyle().bigText(text))
|
||||
.setContentIntent(content)
|
||||
.setOngoing(true)
|
||||
.addAction(action("HIGH", "high", 2))
|
||||
.addAction(action("LOW", "low", 3))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Notification.Action action(String title, String command, int requestCode) {
|
||||
Intent intent = new Intent(this, ActionReceiver.class);
|
||||
intent.setAction(ACTION_COMMAND);
|
||||
intent.putExtra(EXTRA_COMMAND, command);
|
||||
PendingIntent pending = PendingIntent.getBroadcast(
|
||||
this, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new Notification.Action.Builder(null, title, pending).build();
|
||||
}
|
||||
|
||||
private String oneLine(String text) {
|
||||
return text.replace('\n', ' ');
|
||||
}
|
||||
|
||||
private void createChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL, "IntelliRAT status", NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription("Current IntelliRAT network profile");
|
||||
((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
|
||||
.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class IntelliRatTileService extends TileService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
super.onStartListening();
|
||||
refreshTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
updateTile(Tile.STATE_INACTIVE, "IntelliRAT");
|
||||
executor.execute(() -> {
|
||||
Backend.Result status = Backend.run("status", "0");
|
||||
if (!status.ok()) {
|
||||
updateTile(Tile.STATE_INACTIVE, "IntelliRAT");
|
||||
return;
|
||||
}
|
||||
String command = status.output.contains("state=HIGH") ? "low" : "high";
|
||||
Backend.Result result = Backend.run(command, "0");
|
||||
updateTile(result.ok() && "high".equals(command)
|
||||
? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE, "IntelliRAT");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
executor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void refreshTile() {
|
||||
executor.execute(() -> {
|
||||
Backend.Result result = Backend.run("status", "0");
|
||||
if (!result.ok()) {
|
||||
updateTile(Tile.STATE_INACTIVE, "LOW");
|
||||
} else if (result.output.contains("state=HIGH")) {
|
||||
updateTile(Tile.STATE_ACTIVE, "HIGH");
|
||||
} else if (result.output.contains("state=LOW")) {
|
||||
updateTile(Tile.STATE_INACTIVE, "LOW");
|
||||
} else {
|
||||
updateTile(Tile.STATE_UNAVAILABLE, "IntelliRAT");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTile(int state, String label) {
|
||||
getMainExecutor().execute(() -> {
|
||||
Tile tile = getQsTile();
|
||||
if (tile == null) return;
|
||||
tile.setState(state);
|
||||
tile.setLabel("IntelliRAT");
|
||||
tile.setContentDescription("IntelliRAT " + label);
|
||||
tile.updateTile();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.axp.intellirat;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.provider.Settings;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MainActivity extends Activity {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private TextView status;
|
||||
private EditText slot;
|
||||
private EditText delay;
|
||||
private EditText unlockDelay;
|
||||
private EditText exceptions;
|
||||
private Switch automatic;
|
||||
private final Handler applyHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable applyTask = this::configureAutomaticPolicy;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
setTitle("IntelliRAT");
|
||||
buildUi();
|
||||
requestNotifications();
|
||||
requestUsageAccessIfNeeded();
|
||||
startService(new Intent(this, IntelliRatService.class));
|
||||
runCommand("status");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
executor.shutdownNow();
|
||||
applyHandler.removeCallbacks(applyTask);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setPadding(32, 28, 32, 28);
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText("IntelliRAT");
|
||||
title.setTextSize(28);
|
||||
title.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
|
||||
root.addView(title, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
TextView subtitle = new TextView(this);
|
||||
subtitle.setText("Android 16 reversible network profile controller");
|
||||
subtitle.setPadding(0, 4, 0, 24);
|
||||
root.addView(subtitle, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
LinearLayout slotRow = new LinearLayout(this);
|
||||
slotRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||
TextView slotLabel = new TextView(this);
|
||||
slotLabel.setText("Slot: ");
|
||||
slotRow.addView(slotLabel, new LinearLayout.LayoutParams(-2, -2));
|
||||
slot = new EditText(this);
|
||||
slot.setText("0");
|
||||
slot.setSingleLine(true);
|
||||
slot.setInputType(2);
|
||||
slotRow.addView(slot, new LinearLayout.LayoutParams(120, -2));
|
||||
root.addView(slotRow, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
TextView mode = new TextView(this);
|
||||
mode.setText("Automatic lock policy: LOW after lock delay, HIGH after unlock.");
|
||||
mode.setPadding(0, 20, 0, 12);
|
||||
root.addView(mode, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
TextView lockLabel = new TextView(this);
|
||||
lockLabel.setText("Lock LOW delay (seconds)");
|
||||
root.addView(lockLabel, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
LinearLayout policy = new LinearLayout(this);
|
||||
policy.setGravity(Gravity.CENTER_VERTICAL);
|
||||
automatic = new Switch(this);
|
||||
automatic.setText("Automatic lock policy");
|
||||
automatic.setChecked(getSharedPreferences(IntelliRatService.PREFS, MODE_PRIVATE)
|
||||
.getBoolean(IntelliRatService.AUTO_ENABLED, false));
|
||||
automatic.setOnCheckedChangeListener((button, checked) -> scheduleAutomaticApply());
|
||||
policy.addView(automatic, new LinearLayout.LayoutParams(0, -2, 1));
|
||||
delay = new EditText(this);
|
||||
delay.setText(String.valueOf(getSharedPreferences(IntelliRatService.PREFS, MODE_PRIVATE)
|
||||
.getInt(IntelliRatService.AUTO_DELAY_SECONDS, 30)));
|
||||
delay.setHint("seconds");
|
||||
delay.setSingleLine(true);
|
||||
delay.setInputType(2);
|
||||
policy.addView(delay, new LinearLayout.LayoutParams(220, -2));
|
||||
addApplyWatcher(delay);
|
||||
root.addView(policy, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
TextView unlockLabel = new TextView(this);
|
||||
unlockLabel.setText("Unlock HIGH delay (seconds; 0 = immediate)");
|
||||
root.addView(unlockLabel, new LinearLayout.LayoutParams(-1, -2));
|
||||
unlockDelay = new EditText(this);
|
||||
unlockDelay.setText(String.valueOf(getSharedPreferences(IntelliRatService.PREFS, MODE_PRIVATE)
|
||||
.getInt(IntelliRatService.AUTO_UNLOCK_DELAY_SECONDS, 0)));
|
||||
unlockDelay.setHint("unlock HIGH seconds (0 = immediate)");
|
||||
unlockDelay.setSingleLine(true);
|
||||
unlockDelay.setInputType(2);
|
||||
addApplyWatcher(unlockDelay);
|
||||
root.addView(unlockDelay, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
Button selectExceptions = new Button(this);
|
||||
selectExceptions.setText("Select exception apps");
|
||||
selectExceptions.setOnClickListener(view -> showExceptionPicker());
|
||||
root.addView(selectExceptions, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
exceptions = new EditText(this);
|
||||
exceptions.setHint("Exception packages, one per line (for example com.spotify.music)");
|
||||
exceptions.setInputType(android.text.InputType.TYPE_CLASS_TEXT
|
||||
| android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
exceptions.setMinLines(2);
|
||||
exceptions.setText(getSharedPreferences(IntelliRatService.PREFS, MODE_PRIVATE)
|
||||
.getString(IntelliRatService.AUTO_EXCEPTIONS, ""));
|
||||
addApplyWatcher(exceptions);
|
||||
root.addView(exceptions, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
LinearLayout actions = new LinearLayout(this);
|
||||
actions.setOrientation(LinearLayout.VERTICAL);
|
||||
addButton(actions, "Refresh status", "status");
|
||||
addButton(actions, "HIGH LTE / 3G / 2G", "high");
|
||||
addButton(actions, "LOW 3G / 2G", "low");
|
||||
root.addView(actions, new LinearLayout.LayoutParams(-1, -2));
|
||||
|
||||
status = new TextView(this);
|
||||
status.setTextIsSelectable(true);
|
||||
status.setTypeface(Typeface.MONOSPACE);
|
||||
status.setPadding(0, 24, 0, 0);
|
||||
status.setText("Waiting for root backend...");
|
||||
ScrollView scroll = new ScrollView(this);
|
||||
scroll.addView(status);
|
||||
root.addView(scroll, new LinearLayout.LayoutParams(-1, 0, 1));
|
||||
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
private void addButton(LinearLayout parent, String label, String command) {
|
||||
Button button = new Button(this);
|
||||
button.setText(label);
|
||||
button.setOnClickListener(view -> runCommand(command));
|
||||
parent.addView(button, new LinearLayout.LayoutParams(-1, -2));
|
||||
}
|
||||
|
||||
private void runCommand(String command) {
|
||||
String selectedSlot = slot == null ? "0" : slot.getText().toString().trim();
|
||||
status.setText("Running: " + command + " slot " + selectedSlot + "...");
|
||||
executor.execute(() -> {
|
||||
Backend.Result result = Backend.run(command, selectedSlot);
|
||||
runOnUiThread(() -> status.setText(
|
||||
(result.ok() ? "OK\n" : "FAILED (" + result.exitCode + ")\n")
|
||||
+ result.output));
|
||||
});
|
||||
}
|
||||
|
||||
private void configureAutomaticPolicy() {
|
||||
String value = delay == null ? "30" : delay.getText().toString().trim();
|
||||
int seconds;
|
||||
try {
|
||||
seconds = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
status.setText("Invalid delay: use 5-3600 seconds");
|
||||
return;
|
||||
}
|
||||
if (seconds < 5 || seconds > 3600) {
|
||||
status.setText("Invalid delay: use 5-3600 seconds");
|
||||
return;
|
||||
}
|
||||
Intent service = new Intent(this, IntelliRatService.class)
|
||||
.setAction(IntelliRatService.ACTION_AUTO_CONFIG)
|
||||
.putExtra(IntelliRatService.EXTRA_AUTO_ENABLED,
|
||||
automatic != null && automatic.isChecked())
|
||||
.putExtra(IntelliRatService.EXTRA_AUTO_DELAY_SECONDS, seconds)
|
||||
.putExtra(IntelliRatService.EXTRA_AUTO_UNLOCK_DELAY_SECONDS,
|
||||
parseUnlockDelay())
|
||||
.putExtra(IntelliRatService.EXTRA_AUTO_EXCEPTIONS,
|
||||
exceptions == null ? "" : exceptions.getText().toString());
|
||||
startService(service);
|
||||
status.setText("Applying automatic policy...");
|
||||
}
|
||||
|
||||
private void addApplyWatcher(EditText field) {
|
||||
field.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) {
|
||||
scheduleAutomaticApply();
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) { }
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleAutomaticApply() {
|
||||
applyHandler.removeCallbacks(applyTask);
|
||||
applyHandler.postDelayed(applyTask, 600L);
|
||||
}
|
||||
|
||||
private void requestUsageAccessIfNeeded() {
|
||||
AppOpsManager appOps = (AppOpsManager) getSystemService(APP_OPS_SERVICE);
|
||||
int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
|
||||
Process.myUid(), getPackageName());
|
||||
if (mode != AppOpsManager.MODE_ALLOWED) {
|
||||
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
|
||||
}
|
||||
}
|
||||
|
||||
private int parseUnlockDelay() {
|
||||
String value = unlockDelay == null ? "0" : unlockDelay.getText().toString().trim();
|
||||
int seconds;
|
||||
try {
|
||||
seconds = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.min(3600, seconds));
|
||||
}
|
||||
|
||||
private void showExceptionPicker() {
|
||||
PackageManager packageManager = getPackageManager();
|
||||
List<ApplicationInfo> applications = new ArrayList<>();
|
||||
for (ApplicationInfo application : packageManager.getInstalledApplications(
|
||||
PackageManager.GET_META_DATA)) {
|
||||
if ((application.flags & ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
|| application.packageName.equals("android")
|
||||
|| application.packageName.equals(getPackageName())) {
|
||||
continue;
|
||||
}
|
||||
applications.add(application);
|
||||
}
|
||||
Collections.sort(applications, Comparator.comparing(application ->
|
||||
packageManager.getApplicationLabel(application).toString().toLowerCase()));
|
||||
String[] labels = new String[applications.size()];
|
||||
boolean[] checked = new boolean[applications.size()];
|
||||
Set<String> selected = new HashSet<>();
|
||||
for (String value : exceptions.getText().toString().split("[,\\n\\r ]+")) {
|
||||
if (!value.trim().isEmpty()) selected.add(value.trim());
|
||||
}
|
||||
for (int i = 0; i < applications.size(); i++) {
|
||||
ApplicationInfo application = applications.get(i);
|
||||
labels[i] = packageManager.getApplicationLabel(application).toString();
|
||||
checked[i] = selected.contains(application.packageName);
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Keep HIGH for selected apps")
|
||||
.setMultiChoiceItems(labels, checked, (dialog, which, isChecked) -> {
|
||||
checked[which] = isChecked;
|
||||
})
|
||||
.setPositiveButton("Use selected", (dialog, which) -> {
|
||||
StringBuilder value = new StringBuilder();
|
||||
for (int i = 0; i < applications.size(); i++) {
|
||||
if (checked[i]) {
|
||||
if (value.length() > 0) value.append('\n');
|
||||
value.append(applications.get(i).packageName);
|
||||
}
|
||||
}
|
||||
exceptions.setText(value.toString());
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void requestNotifications() {
|
||||
if (Build.VERSION.SDK_INT >= 33
|
||||
&& checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# IntelliRAT v0.1 manual prototype.
|
||||
# Masks are Android TelephonyManager bitmasks encoded as 20-bit binary strings.
|
||||
# HIGH = LTE + UMTS/HSPA + GSM families. LOW = UMTS/HSPA + GSM families.
|
||||
TARGET_SLOT=0
|
||||
HIGH_MASK=00011101001110000111
|
||||
LOW_MASK=00011100001110000111
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
INSTALL_LOG=/data/local/tmp/intellirat-install.log
|
||||
INSTALL_APK=/data/local/tmp/intellirat-install.apk
|
||||
|
||||
printf '%s phase=customize start\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$INSTALL_LOG"
|
||||
set_perm "$MODPATH/service.sh" 0 0 0755
|
||||
set_perm "$MODPATH/uninstall.sh" 0 0 0755
|
||||
set_perm "$MODPATH/system/bin/intellirat" 0 0 0755
|
||||
set_perm "$MODPATH/payload/IntelliRAT.apk" 0 0 0644
|
||||
|
||||
package_ready=0
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 30 ]; do
|
||||
if cmd package path android >/dev/null 2>&1; then
|
||||
package_ready=1
|
||||
break
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$package_ready" != 1 ]; then
|
||||
printf '%s phase=customize error=package_service_timeout\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$INSTALL_LOG"
|
||||
abort "IntelliRAT: Android package service did not become available"
|
||||
fi
|
||||
|
||||
if ! cp "$MODPATH/payload/IntelliRAT.apk" "$INSTALL_APK"; then
|
||||
printf '%s phase=customize error=copy_failed\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$INSTALL_LOG"
|
||||
abort "IntelliRAT: could not stage APK for PackageManager"
|
||||
fi
|
||||
chmod 0644 "$INSTALL_APK"
|
||||
install_result=$(pm install -r --user 0 "$INSTALL_APK" 2>&1 || true)
|
||||
printf '%s phase=customize result=%s\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$install_result" >> "$INSTALL_LOG"
|
||||
rm -f "$INSTALL_APK"
|
||||
case "$install_result" in
|
||||
*Success*) ;;
|
||||
*) abort "IntelliRAT APK installation failed: $install_result" ;;
|
||||
esac
|
||||
|
||||
for user in $(cmd user list 2>/dev/null | awk -F '[{}:]' '/UserInfo/ {print $2}'); do
|
||||
if [ "$user" != 0 ]; then
|
||||
pm uninstall --user "$user" com.axp.intellirat >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
TILE='custom(com.axp.intellirat/.IntelliRatTileService)'
|
||||
tiles=$(settings get secure sysui_qs_tiles 2>/dev/null || true)
|
||||
case ",$tiles," in
|
||||
*",$TILE,"*) ;;
|
||||
*) settings put secure sysui_qs_tiles "$tiles,$TILE" ;;
|
||||
esac
|
||||
printf '%s phase=customize integration_complete\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$INSTALL_LOG"
|
||||
@@ -0,0 +1,6 @@
|
||||
id=intellirat
|
||||
name=IntelliRAT
|
||||
version=0.2.0
|
||||
versionCode=2
|
||||
author=AXP Resurrection
|
||||
description=Automatic reversible Android 16 RAT profile controller
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
# Initialize IntelliRAT state and register its Quick Settings tile.
|
||||
MODDIR=${0%/*}
|
||||
STATE_DIR=/data/local/tmp/intellirat
|
||||
APK="$MODDIR/payload/IntelliRAT.apk"
|
||||
INSTALL_APK=/data/local/tmp/intellirat-install.apk
|
||||
package_ready=1
|
||||
|
||||
if ! pm path com.axp.intellirat >/dev/null 2>&1 && [ -f "$APK" ]; then
|
||||
if cp "$APK" "$INSTALL_APK" 2>/dev/null; then
|
||||
chmod 0644 "$INSTALL_APK"
|
||||
install_result=$(pm install -r --user 0 "$INSTALL_APK" 2>&1 || true)
|
||||
printf '%s install_result=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$install_result" \
|
||||
>> /data/local/tmp/intellirat-install.log
|
||||
rm -f "$INSTALL_APK"
|
||||
else
|
||||
printf '%s install_error=copy_failed source=%s\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$APK" >> /data/local/tmp/intellirat-install.log
|
||||
fi
|
||||
elif [ "$package_ready" = 1 ] && ! pm path com.axp.intellirat >/dev/null 2>&1; then
|
||||
printf '%s install_error=payload_missing path=%s\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$APK" >> /data/local/tmp/intellirat-install.log
|
||||
fi
|
||||
for user in $(cmd user list 2>/dev/null | awk -F '[{}:]' '/UserInfo/ {print $2}'); do
|
||||
if [ "$user" != 0 ]; then
|
||||
pm uninstall --user "$user" com.axp.intellirat >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
chown 0 2000 "$STATE_DIR"
|
||||
chmod 0770 "$STATE_DIR"
|
||||
|
||||
if [ ! -f "$STATE_DIR/config.conf" ]; then
|
||||
cp "$MODDIR/config/default.conf" "$STATE_DIR/config.conf"
|
||||
chown 0 2000 "$STATE_DIR/config.conf"
|
||||
chmod 0600 "$STATE_DIR/config.conf"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') module_loaded version=0.1.0" \
|
||||
>> "$STATE_DIR/events.log"
|
||||
chown 0 2000 "$STATE_DIR/events.log"
|
||||
chmod 0600 "$STATE_DIR/events.log"
|
||||
|
||||
TILE='custom(com.axp.intellirat/.IntelliRatTileService)'
|
||||
tiles=$(settings get secure sysui_qs_tiles 2>/dev/null || true)
|
||||
case ",$tiles," in
|
||||
*",$TILE,"*) ;;
|
||||
*) settings put secure sysui_qs_tiles "$tiles,$TILE" ;;
|
||||
esac
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
set -u
|
||||
|
||||
STATE_DIR=${INTELLIRAT_STATE_DIR:-/data/local/tmp}
|
||||
CONFIG_FILE=${INTELLIRAT_CONFIG:-$STATE_DIR/config.conf}
|
||||
LOG_FILE=${INTELLIRAT_LOG:-$STATE_DIR/events.log}
|
||||
CMD_BIN=${INTELLIRAT_CMD:-cmd}
|
||||
DUMPSYS_BIN=${INTELLIRAT_DUMPSYS:-dumpsys}
|
||||
|
||||
DEFAULT_SLOT=0
|
||||
DEFAULT_HIGH_MASK=00011101001110000111
|
||||
DEFAULT_LOW_MASK=00011100001110000111
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
IntelliRAT v0.1 manual prototype
|
||||
|
||||
Usage:
|
||||
intellirat status [slot]
|
||||
intellirat capture [slot]
|
||||
intellirat high [slot]
|
||||
intellirat low [slot]
|
||||
intellirat restore [slot]
|
||||
intellirat --dry-run high|low|restore [slot]
|
||||
|
||||
The prototype never changes RAT automatically. Capture the current state before
|
||||
the first transition. Restore returns the captured user network mask.
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf 'IntelliRAT: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
load_config() {
|
||||
TARGET_SLOT=$DEFAULT_SLOT
|
||||
HIGH_MASK=$DEFAULT_HIGH_MASK
|
||||
LOW_MASK=$DEFAULT_LOW_MASK
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# The file is created root-only by service.sh; reject unsafe ownership
|
||||
# only when the platform exposes stat in the available shell.
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
case "$TARGET_SLOT" in
|
||||
''|*[!0-9]*) fail "invalid TARGET_SLOT" ;;
|
||||
esac
|
||||
case "$HIGH_MASK:$LOW_MASK" in
|
||||
*[!01:]*|:*) fail "masks must be non-empty binary strings" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_state_dir() {
|
||||
mkdir -p "$STATE_DIR" || fail "cannot create $STATE_DIR"
|
||||
if [ "$STATE_DIR" = /data/local/tmp ]; then
|
||||
return
|
||||
fi
|
||||
chown 0 2000 "$STATE_DIR" 2>/dev/null || true
|
||||
chmod 0770 "$STATE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
log_event() {
|
||||
ensure_state_dir
|
||||
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >> "$LOG_FILE"
|
||||
chmod 0600 "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
slot_or_default() {
|
||||
if [ "${1:-}" = '' ]; then
|
||||
printf '%s' "$TARGET_SLOT"
|
||||
else
|
||||
case "$1" in
|
||||
''|*[!0-9]*) fail "invalid slot: $1" ;;
|
||||
esac
|
||||
printf '%s' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
state_file() {
|
||||
printf '%s/intellirat-sub-%s.state' "$STATE_DIR" "$1"
|
||||
}
|
||||
|
||||
read_user_names() {
|
||||
slot=$1
|
||||
result=$(
|
||||
"$CMD_BIN" phone get-allowed-network-types-for-users -s "$slot" 2>&1
|
||||
) || fail "cannot read network types for slot $slot: $result"
|
||||
case "$result" in
|
||||
*'No valid subscription found'*|*'No valid NETWORK_TYPES_BITMASK'*)
|
||||
fail "no valid subscription for slot $slot: $result"
|
||||
;;
|
||||
esac
|
||||
result=$(printf '%s' "$result" | tr -d '[:space:]\r\n')
|
||||
[ -n "$result" ] || fail "empty network type response for slot $slot"
|
||||
printf '%s' "$result"
|
||||
}
|
||||
|
||||
network_names_to_decimal() {
|
||||
names=$1
|
||||
mask=0
|
||||
old_ifs=$IFS
|
||||
IFS='|'
|
||||
# shellcheck disable=SC2086
|
||||
for name in $names; do
|
||||
case "$name" in
|
||||
GPRS) mask=$((mask | 1)) ;;
|
||||
EDGE) mask=$((mask | 2)) ;;
|
||||
UMTS) mask=$((mask | 4)) ;;
|
||||
CDMA) mask=$((mask | 8)) ;;
|
||||
EVDO_0) mask=$((mask | 16)) ;;
|
||||
EVDO_A) mask=$((mask | 32)) ;;
|
||||
1xRTT) mask=$((mask | 64)) ;;
|
||||
HSDPA) mask=$((mask | 128)) ;;
|
||||
HSUPA) mask=$((mask | 256)) ;;
|
||||
HSPA) mask=$((mask | 512)) ;;
|
||||
HSPA+) mask=$((mask | 16384)) ;;
|
||||
IDEN) mask=$((mask | 1024)) ;;
|
||||
EVDO_B) mask=$((mask | 2048)) ;;
|
||||
LTE) mask=$((mask | 4096)) ;;
|
||||
EHRPD) mask=$((mask | 8192)) ;;
|
||||
HSPAP) mask=$((mask | 16384)) ;;
|
||||
GSM) mask=$((mask | 32768)) ;;
|
||||
TD_SCDMA) mask=$((mask | 65536)) ;;
|
||||
IWLAN) mask=$((mask | 131072)) ;;
|
||||
LTE_CA) mask=$((mask | 4096)) ;;
|
||||
NR) mask=$((mask | 524288)) ;;
|
||||
UNKNOWN|NONE) ;;
|
||||
*) IFS=$old_ifs; fail "unknown network type from telephony: $name" ;;
|
||||
esac
|
||||
done
|
||||
IFS=$old_ifs
|
||||
printf '%s' "$mask"
|
||||
}
|
||||
|
||||
decimal_to_binary() {
|
||||
mask=$1
|
||||
output=
|
||||
bit=19
|
||||
while [ "$bit" -ge 0 ]; do
|
||||
value=$((1 << bit))
|
||||
if [ $((mask & value)) -ne 0 ]; then
|
||||
output="${output}1"
|
||||
else
|
||||
output="${output}0"
|
||||
fi
|
||||
bit=$((bit - 1))
|
||||
done
|
||||
printf '%s' "$output"
|
||||
}
|
||||
|
||||
read_user_binary() {
|
||||
names=$(read_user_names "$1")
|
||||
decimal=$(network_names_to_decimal "$names")
|
||||
decimal_to_binary "$decimal"
|
||||
}
|
||||
|
||||
call_in_progress() {
|
||||
dump=$(
|
||||
"$DUMPSYS_BIN" telecom 2>/dev/null || true
|
||||
)
|
||||
current_calls=$(printf '%s\n' "$dump" | awk '
|
||||
/^ All calls:/ { in_calls=1; next }
|
||||
/^ Active dialing, or connecting calls:/ { in_calls=1; next }
|
||||
/^ Ringing calls:/ { in_calls=1; next }
|
||||
/^ Holding calls:/ { in_calls=1; next }
|
||||
/^[[:space:]]*[A-Za-z][A-Za-z _-]*:$/ { in_calls=0 }
|
||||
in_calls { print }
|
||||
')
|
||||
printf '%s\n' "$current_calls" | grep -Eq \
|
||||
'(^|[^A-Za-z])(ACTIVE|RINGING|DIALING|CONNECTING)([^A-Za-z]|$)|mCallState=(ACTIVE|RINGING|DIALING|CONNECTING)'
|
||||
}
|
||||
|
||||
write_state() {
|
||||
slot=$1
|
||||
original_names=$2
|
||||
original_binary=$3
|
||||
file=$(state_file "$slot")
|
||||
tmp="${file}.tmp.$$"
|
||||
{
|
||||
printf 'original_names=%s\n' "$original_names"
|
||||
printf 'original_mask=%s\n' "$original_binary"
|
||||
printf 'captured_at=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
} > "$tmp" || fail "cannot write $file"
|
||||
chmod 0600 "$tmp"
|
||||
mv "$tmp" "$file" || fail "cannot commit $file"
|
||||
}
|
||||
|
||||
capture() {
|
||||
slot=$1
|
||||
ensure_state_dir
|
||||
names=$(read_user_names "$slot")
|
||||
decimal=$(network_names_to_decimal "$names")
|
||||
binary=$(decimal_to_binary "$decimal")
|
||||
file=$(state_file "$slot")
|
||||
if [ -f "$file" ] && [ "${INTELLIRAT_FORCE_CAPTURE:-0}" != 1 ]; then
|
||||
fail "state already captured for slot $slot; set INTELLIRAT_FORCE_CAPTURE=1 to replace it"
|
||||
fi
|
||||
write_state "$slot" "$names" "$binary"
|
||||
log_event "capture slot=$slot mask=$binary names=$names"
|
||||
printf 'captured slot=%s mask=%s names=%s\n' "$slot" "$binary" "$names"
|
||||
}
|
||||
|
||||
get_original_mask() {
|
||||
file=$(state_file "$1")
|
||||
[ -f "$file" ] || {
|
||||
printf '%s' "$HIGH_MASK"
|
||||
return
|
||||
}
|
||||
value=$(grep '^original_mask=' "$file" | cut -d= -f2-)
|
||||
case "$value" in
|
||||
''|*[!01]*) fail "invalid saved state for slot $1" ;;
|
||||
esac
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
apply_mask() {
|
||||
slot=$1
|
||||
target=$2
|
||||
reason=$3
|
||||
current=$(read_user_binary "$slot")
|
||||
if [ "$current" = "$target" ]; then
|
||||
log_event "noop slot=$slot state=$reason mask=$target"
|
||||
printf 'already state=%s slot=%s mask=%s\n' "$reason" "$slot" "$target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if call_in_progress; then
|
||||
fail "telephony call is active; transition refused"
|
||||
fi
|
||||
|
||||
if [ "${INTELLIRAT_DRY_RUN:-0}" = 1 ]; then
|
||||
printf 'dry-run cmd phone set-allowed-network-types-for-users -s %s %s\n' \
|
||||
"$slot" "$target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
result=$(
|
||||
"$CMD_BIN" phone set-allowed-network-types-for-users -s "$slot" "$target" 2>&1
|
||||
) || fail "network transition failed: $result"
|
||||
case "$result" in
|
||||
*completed*) ;;
|
||||
*) fail "telephony did not confirm transition: $result" ;;
|
||||
esac
|
||||
|
||||
actual=$(read_user_binary "$slot")
|
||||
[ "$actual" = "$target" ] || fail "verification mismatch: requested=$target actual=$actual"
|
||||
log_event "transition slot=$slot previous=$current state=$reason requested=$target actual=$actual"
|
||||
printf 'transition slot=%s state=%s previous=%s actual=%s\n' \
|
||||
"$slot" "$reason" "$current" "$actual"
|
||||
}
|
||||
|
||||
status() {
|
||||
slot=$1
|
||||
names=$(read_user_names "$slot")
|
||||
decimal=$(network_names_to_decimal "$names")
|
||||
current=$(decimal_to_binary "$decimal")
|
||||
state=UNKNOWN
|
||||
[ "$current" = "$HIGH_MASK" ] && state=HIGH
|
||||
[ "$current" = "$LOW_MASK" ] && state=LOW
|
||||
saved=NO
|
||||
[ -f "$(state_file "$slot")" ] && saved=YES
|
||||
printf 'slot=%s state=%s mask=%s names=%s saved_original=%s\n' \
|
||||
"$slot" "$state" "$current" "$names" "$saved"
|
||||
}
|
||||
|
||||
main() {
|
||||
dry_run=0
|
||||
if [ "${1:-}" = '--dry-run' ]; then
|
||||
dry_run=1
|
||||
INTELLIRAT_DRY_RUN=1
|
||||
export INTELLIRAT_DRY_RUN
|
||||
shift
|
||||
fi
|
||||
load_config
|
||||
ensure_state_dir
|
||||
|
||||
command=${1:-help}
|
||||
shift || true
|
||||
slot=$(slot_or_default "${1:-}")
|
||||
|
||||
case "$command" in
|
||||
help|-h|--help) usage ;;
|
||||
status) status "$slot" ;;
|
||||
capture) capture "$slot" ;;
|
||||
high) apply_mask "$slot" "$HIGH_MASK" HIGH ;;
|
||||
low) apply_mask "$slot" "$LOW_MASK" LOW ;;
|
||||
restore) apply_mask "$slot" "$(get_original_mask "$slot")" RESTORE ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
for user in $(cmd user list 2>/dev/null | awk -F '[{}:]' '/UserInfo/ {print $2}'); do
|
||||
pm uninstall --user "$user" com.axp.intellirat >/dev/null 2>&1 || true
|
||||
done
|
||||
rm -f /data/local/tmp/intellirat-sub-*.state
|
||||
rm -f /data/local/tmp/intellirat-events.log
|
||||
Reference in new issue
Block a user