009: publish AXPR23 final source and public documentation

This commit is contained in:
sunfish-admin committed 2026-11-06 21:11:35 +01:00
1 parent 72930e0fd6
commit 2c714d8d34
35 files changed
+2498 -403

No files matched your search

+10
View File
@@ -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>
+1
View File
@@ -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);
}
}
}