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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user