diff --git a/apk/controller/.idea/caches/deviceStreaming.xml b/apk/controller/.idea/caches/deviceStreaming.xml
index 2829725..23267a6 100644
--- a/apk/controller/.idea/caches/deviceStreaming.xml
+++ b/apk/controller/.idea/caches/deviceStreaming.xml
@@ -832,6 +832,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1218,6 +1230,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apk/controller/.idea/deploymentTargetSelector.xml b/apk/controller/.idea/deploymentTargetSelector.xml
index ed6d621..aced41e 100644
--- a/apk/controller/.idea/deploymentTargetSelector.xml
+++ b/apk/controller/.idea/deploymentTargetSelector.xml
@@ -4,10 +4,10 @@
-
+
-
+
diff --git a/apk/controller/app/build.gradle b/apk/controller/app/build.gradle
index 310f0e0..875d8bf 100644
--- a/apk/controller/app/build.gradle
+++ b/apk/controller/app/build.gradle
@@ -9,8 +9,8 @@ android {
applicationId "org.iiab.controller"
minSdkVersion 24
targetSdkVersion 34
- versionCode 22
- versionName "v0.1.26alpha"
+ versionCode 23
+ versionName "v0.1.27alpha"
setProperty("archivesBaseName", "$applicationId-$versionName")
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
diff --git a/apk/controller/app/release/output-metadata.json b/apk/controller/app/release/output-metadata.json
index 74e26b1..05c8d4b 100644
--- a/apk/controller/app/release/output-metadata.json
+++ b/apk/controller/app/release/output-metadata.json
@@ -11,9 +11,9 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
- "versionCode": 22,
- "versionName": "v0.1.26alpha",
- "outputFile": "org.iiab.controller-v0.1.26alpha-release.apk"
+ "versionCode": 23,
+ "versionName": "v0.1.27alpha",
+ "outputFile": "org.iiab.controller-v0.1.27alpha-release.apk"
}
],
"elementType": "File",
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java
index e11e19e..6115946 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java
@@ -22,6 +22,7 @@ import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
+import android.content.ComponentName;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -47,7 +48,6 @@ import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
-import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.BufferedReader;
@@ -55,10 +55,13 @@ import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
-import java.util.concurrent.Executor;
-import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.text.SimpleDateFormat;
+import java.util.concurrent.Executor;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "IIAB-MainActivity";
@@ -95,11 +98,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private ProgressBar logProgress;
private ActivityResultLauncher vpnPermissionLauncher;
- private ActivityResultLauncher requestPermissionLauncher;
- private ActivityResultLauncher notificationPermissionLauncher;
+ private ActivityResultLauncher requestPermissionsLauncher;
private boolean isReadingLogs = false;
- private Handler sizeUpdateHandler = new Handler();
+ private final Handler sizeUpdateHandler = new Handler();
private Runnable sizeUpdateRunnable;
private final BroadcastReceiver logReceiver = new BroadcastReceiver() {
@@ -120,38 +122,35 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
prefs = new Preferences(this);
setContentView(R.layout.main);
- // Initialize Result Launchers
+ // 1. Initialize Result Launchers
vpnPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK && prefs.getEnable()) {
connectVpn();
}
+ // Chain: Check battery after VPN dialog
+ checkBatteryOptimizations();
}
);
- requestPermissionLauncher = registerForActivityResult(
- new ActivityResultContracts.RequestPermission(),
- isGranted -> {
- if (isGranted) {
- addToLog("Termux RUN_COMMAND permission granted");
- } else {
- addToLog("Termux permission denied. Watchdog stimulus may fail.");
- }
- }
- );
-
- notificationPermissionLauncher = registerForActivityResult(
- new ActivityResultContracts.RequestPermission(),
- isGranted -> {
- if (isGranted) {
- addToLog("Notification permission granted");
- } else {
- addToLog("Notification permission denied. Status visibility may be limited.");
+ // Modern ordered permission requester
+ requestPermissionsLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(),
+ result -> {
+ for (Map.Entry entry : result.entrySet()) {
+ if (entry.getKey().equals(TERMUX_PERMISSION)) {
+ addToLog(entry.getValue() ? "Termux permission granted" : "Termux permission denied");
+ } else if (entry.getKey().equals(Manifest.permission.POST_NOTIFICATIONS)) {
+ addToLog(entry.getValue() ? "Notification permission granted" : "Notification permission denied");
+ }
}
+ // Step 2: After system permissions, request VPN permission
+ prepareVpn();
}
);
+ // UI Bindings
edittext_socks_addr = findViewById(R.id.socks_addr);
edittext_socks_udp_addr = findViewById(R.id.socks_udp_addr);
edittext_socks_port = findViewById(R.id.socks_port);
@@ -167,25 +166,40 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
button_apps = findViewById(R.id.apps);
button_save = findViewById(R.id.save);
button_control = findViewById(R.id.control);
-
watchdogControl = findViewById(R.id.watchdog_control);
- watchdogControl.setOnClickListener(this);
-
+
logActions = findViewById(R.id.log_actions);
Button btnClearLog = findViewById(R.id.btn_clear_log);
Button btnCopyLog = findViewById(R.id.btn_copy_log);
- btnClearLog.setOnClickListener(this);
- btnCopyLog.setOnClickListener(this);
-
connectionLog = findViewById(R.id.connection_log);
- connectionLog.setMovementMethod(new ScrollingMovementMethod());
- connectionLog.setTextIsSelectable(true);
-
logProgress = findViewById(R.id.log_progress);
logWarning = findViewById(R.id.log_warning_text);
logSizeText = findViewById(R.id.log_size_text);
+ themeToggle = findViewById(R.id.theme_toggle);
+ versionFooter = findViewById(R.id.version_text);
+ configLayout = findViewById(R.id.config_layout);
+ configLabel = findViewById(R.id.config_label);
+ advancedConfig = findViewById(R.id.advanced_config);
+ advConfigLabel = findViewById(R.id.adv_config_label);
+ logLabel = findViewById(R.id.log_label);
- // Allow internal scrolling by disabling parent intercept
+ // Listeners
+ watchdogControl.setOnClickListener(this);
+ btnClearLog.setOnClickListener(this);
+ btnCopyLog.setOnClickListener(this);
+ themeToggle.setOnClickListener(v -> toggleTheme());
+ configLabel.setOnClickListener(v -> toggleVisibility(configLayout, configLabel, getString(R.string.configuration_label)));
+ advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, getString(R.string.advanced_settings_label)));
+ logLabel.setOnClickListener(v -> handleLogToggle());
+ checkbox_udp_in_tcp.setOnClickListener(this);
+ checkbox_remote_dns.setOnClickListener(this);
+ checkbox_global.setOnClickListener(this);
+ button_apps.setOnClickListener(this);
+ button_save.setOnClickListener(this);
+ button_control.setOnClickListener(this);
+
+ connectionLog.setMovementMethod(new ScrollingMovementMethod());
+ connectionLog.setTextIsSelectable(true);
connectionLog.setOnTouchListener((v, event) -> {
v.getParent().requestDisallowInterceptTouchEvent(true);
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
@@ -193,56 +207,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
return false;
});
-
- configLayout = findViewById(R.id.config_layout);
- configLabel = findViewById(R.id.config_label);
- configLabel.setOnClickListener(v -> toggleVisibility(configLayout, configLabel, getString(R.string.configuration_label)));
- advancedConfig = findViewById(R.id.advanced_config);
- advConfigLabel = findViewById(R.id.adv_config_label);
- advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, getString(R.string.advanced_settings_label)));
-
- logLabel = findViewById(R.id.log_label);
- logLabel.setOnClickListener(v -> {
- boolean isOpening = connectionLog.getVisibility() == View.GONE;
- if (isOpening) {
- readBlackBoxLogs();
- startLogSizeUpdates();
- } else {
- stopLogSizeUpdates();
- }
- toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label));
- logActions.setVisibility(connectionLog.getVisibility());
- if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility());
- });
-
- themeToggle = findViewById(R.id.theme_toggle);
- themeToggle.setOnClickListener(v -> toggleTheme());
applySavedTheme();
-
- versionFooter = findViewById(R.id.version_text);
setVersionFooter();
-
- checkbox_udp_in_tcp.setOnClickListener(this);
- checkbox_remote_dns.setOnClickListener(this);
- checkbox_global.setOnClickListener(this);
- button_apps.setOnClickListener(this);
- button_save.setOnClickListener(this);
- button_control.setOnClickListener(this);
updateUI();
- /* Request permissions */
- checkNotificationPermission();
- checkTermuxPermission();
- checkBatteryOptimizations();
-
- /* Request VPN permission */
- Intent intent = VpnService.prepare(MainActivity.this);
- if (intent != null) {
- vpnPermissionLauncher.launch(intent);
- } else if (prefs.getEnable()) {
- connectVpn();
- }
+ // Start sequential permission chain
+ initiatePermissionChain();
addToLog(getString(R.string.app_started));
@@ -255,20 +226,47 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
};
}
- private void checkNotificationPermission() {
+ private void initiatePermissionChain() {
+ List permissions = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
+ permissions.add(Manifest.permission.POST_NOTIFICATIONS);
}
}
+ if (ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) != PackageManager.PERMISSION_GRANTED) {
+ permissions.add(TERMUX_PERMISSION);
+ }
+
+ if (!permissions.isEmpty()) {
+ requestPermissionsLauncher.launch(permissions.toArray(new String[0]));
+ } else {
+ prepareVpn();
+ }
}
- private void checkTermuxPermission() {
- if (ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) != PackageManager.PERMISSION_GRANTED) {
- requestPermissionLauncher.launch(TERMUX_PERMISSION);
+ private void prepareVpn() {
+ Intent intent = VpnService.prepare(MainActivity.this);
+ if (intent != null) {
+ vpnPermissionLauncher.launch(intent);
+ } else {
+ if (prefs.getEnable()) connectVpn();
+ checkBatteryOptimizations();
}
}
+ private void handleLogToggle() {
+ boolean isOpening = connectionLog.getVisibility() == View.GONE;
+ if (isOpening) {
+ readBlackBoxLogs();
+ startLogSizeUpdates();
+ } else {
+ stopLogSizeUpdates();
+ }
+ toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label));
+ logActions.setVisibility(connectionLog.getVisibility());
+ if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility());
+ }
+
private void startLogSizeUpdates() {
sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable);
sizeUpdateHandler.post(sizeUpdateRunnable);
@@ -302,10 +300,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void readBlackBoxLogs() {
if (isReadingLogs) return;
isReadingLogs = true;
-
- if (logProgress != null) {
- logProgress.setVisibility(View.VISIBLE);
- }
+ if (logProgress != null) logProgress.setVisibility(View.VISIBLE);
new Thread(() -> {
File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt");
@@ -327,8 +322,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
final String result = sb.toString();
-
- // Check rapid growth flag
SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
final boolean isRapid = internalPrefs.getBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false);
@@ -389,13 +382,20 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) {
+ String manufacturer = Build.MANUFACTURER.toLowerCase();
+ String message = getString(R.string.battery_opt_msg);
+
+ if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
+ message += getString(R.string.battery_opt_oppo_extra);
+ } else if (manufacturer.contains("xiaomi")) {
+ message += getString(R.string.battery_opt_xiaomi_extra);
+ }
+
new AlertDialog.Builder(this)
.setTitle(R.string.battery_opt_title)
- .setMessage(R.string.battery_opt_msg)
+ .setMessage(message)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> {
- Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- intent.setData(Uri.parse("package:" + getPackageName()));
- startActivity(intent);
+ openBatterySettings(manufacturer);
})
.setNegativeButton(R.string.cancel, null)
.show();
@@ -403,6 +403,42 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
+ private void openBatterySettings(String manufacturer) {
+ if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
+ try {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("com.coloros.safecenter",
+ "com.coloros.safecenter.permission.startup.StartupAppListActivity"));
+ startActivity(intent);
+ return;
+ } catch (Exception e) {
+ try {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("com.coloros.oppoguardelf",
+ "com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
+ startActivity(intent);
+ return;
+ } catch (Exception e2) {
+ }
+ }
+ } else if (manufacturer.contains("xiaomi")) {
+ try {
+ Intent intent = new Intent("miui.intent.action.APP_BATTERY_SAVER_SETTINGS");
+ intent.setComponent(new ComponentName("com.miui.powerkeeper",
+ "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
+ intent.putExtra("package_name", getPackageName());
+ intent.putExtra("package_label", getString(R.string.app_name));
+ startActivity(intent);
+ return;
+ } catch (Exception e) {
+ }
+ }
+
+ Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ intent.setData(Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ }
+
private void toggleTheme() {
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
int currentMode = AppCompatDelegate.getDefaultNightMode();
@@ -455,7 +491,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(logReceiver, filter);
@@ -512,15 +550,12 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void resetLogFile() {
File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt");
try (PrintWriter pw = new PrintWriter(logFile)) {
- pw.print(""); // Truncate file to 0 bytes
+ pw.print("");
connectionLog.setText("");
addToLog(getString(R.string.log_reset_user));
-
- // Clear rapid growth warning
SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
internalPrefs.edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
if (logWarning != null) logWarning.setVisibility(View.GONE);
-
updateLogSizeUI();
Toast.makeText(this, R.string.log_cleared_toast, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
@@ -560,16 +595,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
showBiometricPrompt();
} else {
BiometricManager biometricManager = BiometricManager.from(this);
-
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
}
-
boolean isSecure = false;
android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (km != null && km.isDeviceSecure()) isSecure = true;
-
if (biometricManager.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS || isSecure) {
addToLog(getString(R.string.user_initiated_conn));
toggleService(false);
@@ -602,15 +634,12 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
toggleService(true);
}
});
-
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
-
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.auth_required_title))
.setSubtitle(getString(R.string.auth_required_subtitle))
.setAllowedAuthenticators(authenticators)
.build();
-
biometricPrompt.authenticate(promptInfo);
}
@@ -623,15 +652,12 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
toggleWatchdog(true);
}
});
-
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
-
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.unlock_watchdog_title))
.setSubtitle(getString(R.string.unlock_watchdog_subtitle))
.setAllowedAuthenticators(authenticators)
.build();
-
biometricPrompt.authenticate(promptInfo);
}
@@ -652,7 +678,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void updateUI() {
boolean vpnActive = prefs.getEnable();
boolean watchdogActive = prefs.getWatchdogEnable();
-
if (vpnActive) {
button_control.setText(R.string.control_disable);
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on));
@@ -660,7 +685,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
button_control.setText(R.string.control_enable);
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off));
}
-
if (watchdogActive) {
watchdogControl.setText(R.string.watchdog_disable);
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on));
@@ -668,7 +692,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
watchdogControl.setText(R.string.watchdog_enable);
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off));
}
-
edittext_socks_addr.setText(prefs.getSocksAddress());
edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress());
edittext_socks_port.setText(String.valueOf(prefs.getSocksPort()));
@@ -681,7 +704,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
checkbox_global.setChecked(prefs.getGlobal());
checkbox_udp_in_tcp.setChecked(prefs.getUdpInTcp());
checkbox_remote_dns.setChecked(prefs.getRemoteDns());
-
boolean editable = !vpnActive;
edittext_socks_addr.setEnabled(editable);
edittext_socks_port.setEnabled(editable);
diff --git a/apk/controller/app/src/main/res/values/strings.xml b/apk/controller/app/src/main/res/values/strings.xml
index 0f465c7..726a819 100644
--- a/apk/controller/app/src/main/res/values/strings.xml
+++ b/apk/controller/app/src/main/res/values/strings.xml
@@ -76,4 +76,8 @@
[Termux] Pulse Error (exit %1$d): %2$s
Size: %1$s / 10MB
+
+
+ \n\nOPPO/Realme detected: Please ensure you also enable \'Allow background activity\' in this app\'s settings.
+ \n\nXiaomi detected: Please set battery saver to \'No restrictions\' for this app.