Compare commits

...

2 Commits

5 changed files with 204 additions and 238 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-07T04:28:01.557850134Z"> <DropdownSelection timestamp="2026-03-07T08:21:48.428800901Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=a026a310" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=69K7MB899PKJGQBI" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -3,7 +3,6 @@
package="org.iiab.controller" package="org.iiab.controller"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- Android 11+ Package Visibility -->
<queries> <queries>
<package android:name="com.termux" /> <package android:name="com.termux" />
</queries> </queries>
@ -13,7 +12,6 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.IIABController"> android:theme="@style/Theme.IIABController">
<!-- VPN Service (Network Layer) -->
<service android:name=".TProxyService" android:process=":native" <service android:name=".TProxyService" android:process=":native"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true" android:exported="true"
@ -21,34 +19,28 @@
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="VPN service"/>
android:value="VPN service"/>
</service> </service>
<!-- Watchdog Service (Keep-Alive Layer) -->
<service android:name=".WatchdogService" <service android:name=".WatchdogService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Watchdog and Heartbeat"/>
android:value="Watchdog and Heartbeat"/>
</service> </service>
<receiver android:enabled="true" android:name=".ServiceReceiver" <receiver android:enabled="true" android:name=".ServiceReceiver" android:exported="true">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- VPN Recovery Receiver (The Boomerang) -->
<receiver android:name=".VpnRecoveryReceiver" android:exported="false"> <receiver android:name=".VpnRecoveryReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="org.iiab.controller.RECOVER_VPN" /> <action android:name="org.iiab.controller.RECOVER_VPN" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- Termux Result Callback Receiver -->
<receiver android:name=".TermuxCallbackReceiver" android:exported="false"> <receiver android:name=".TermuxCallbackReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="org.iiab.controller.TERMUX_OUTPUT" /> <action android:name="org.iiab.controller.TERMUX_OUTPUT" />
@ -62,7 +54,6 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".AppListActivity" android:label="@string/app_name"/> <activity android:name=".AppListActivity" android:label="@string/app_name"/>
@ -73,13 +64,14 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="com.termux.permission.RUN_COMMAND" /> <uses-permission android:name="com.termux.permission.RUN_COMMAND" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" android:minSdkVersion="34" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
android:minSdkVersion="34" />
</manifest> </manifest>

View File

@ -129,12 +129,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (result.getResultCode() == RESULT_OK && prefs.getEnable()) { if (result.getResultCode() == RESULT_OK && prefs.getEnable()) {
connectVpn(); connectVpn();
} }
// Chain: Check battery after VPN dialog
checkBatteryOptimizations(); checkBatteryOptimizations();
} }
); );
// Modern ordered permission requester
requestPermissionsLauncher = registerForActivityResult( requestPermissionsLauncher = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultContracts.RequestMultiplePermissions(),
result -> { result -> {
@ -145,7 +143,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
addToLog(entry.getValue() ? "Notification permission granted" : "Notification permission denied"); addToLog(entry.getValue() ? "Notification permission granted" : "Notification permission denied");
} }
} }
// Step 2: After system permissions, request VPN permission
prepareVpn(); prepareVpn();
} }
); );
@ -211,8 +208,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
applySavedTheme(); applySavedTheme();
setVersionFooter(); setVersionFooter();
updateUI(); updateUI();
// Start sequential permission chain
initiatePermissionChain(); initiatePermissionChain();
addToLog(getString(R.string.app_started)); addToLog(getString(R.string.app_started));
@ -394,9 +389,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.battery_opt_title) .setTitle(R.string.battery_opt_title)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> { .setPositiveButton(R.string.go_to_settings, (dialog, which) -> openBatterySettings(manufacturer))
openBatterySettings(manufacturer);
})
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
@ -407,34 +400,29 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (manufacturer.contains("oppo") || manufacturer.contains("realme")) { if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
try { try {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.safecenter", intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity"));
"com.coloros.safecenter.permission.startup.StartupAppListActivity"));
startActivity(intent); startActivity(intent);
return; return;
} catch (Exception e) { } catch (Exception e) {
try { try {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.oppoguardelf", intent.setComponent(new ComponentName("com.coloros.oppoguardelf", "com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
"com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
startActivity(intent); startActivity(intent);
return; return;
} catch (Exception e2) { } catch (Exception e2) {}
}
} }
} else if (manufacturer.contains("xiaomi")) { } else if (manufacturer.contains("xiaomi")) {
try { try {
Intent intent = new Intent("miui.intent.action.APP_BATTERY_SAVER_SETTINGS"); Intent intent = new Intent("miui.intent.action.APP_BATTERY_SAVER_SETTINGS");
intent.setComponent(new ComponentName("com.miui.powerkeeper", intent.setComponent(new ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
"com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
intent.putExtra("package_name", getPackageName()); intent.putExtra("package_name", getPackageName());
intent.putExtra("package_label", getString(R.string.app_name)); intent.putExtra("package_label", getString(R.string.app_name));
startActivity(intent); startActivity(intent);
return; return;
} catch (Exception e) { } catch (Exception e) {}
}
} }
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName())); intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent); startActivity(intent);
} }
@ -442,20 +430,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void toggleTheme() { private void toggleTheme() {
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
int currentMode = AppCompatDelegate.getDefaultNightMode(); int currentMode = AppCompatDelegate.getDefaultNightMode();
int nextMode; int nextMode = (currentMode == AppCompatDelegate.MODE_NIGHT_NO) ? AppCompatDelegate.MODE_NIGHT_YES :
(currentMode == AppCompatDelegate.MODE_NIGHT_YES) ? AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : AppCompatDelegate.MODE_NIGHT_NO;
if (currentMode == AppCompatDelegate.MODE_NIGHT_NO) { sharedPref.edit().putInt("ui_mode", nextMode).apply();
nextMode = AppCompatDelegate.MODE_NIGHT_YES;
} else if (currentMode == AppCompatDelegate.MODE_NIGHT_YES) {
nextMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
} else {
nextMode = AppCompatDelegate.MODE_NIGHT_NO;
}
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt("ui_mode", nextMode);
editor.apply();
AppCompatDelegate.setDefaultNightMode(nextMode); AppCompatDelegate.setDefaultNightMode(nextMode);
updateThemeToggleButton(nextMode); updateThemeToggleButton(nextMode);
} }
@ -468,32 +445,22 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
private void updateThemeToggleButton(int mode) { private void updateThemeToggleButton(int mode) {
if (mode == AppCompatDelegate.MODE_NIGHT_NO) { if (mode == AppCompatDelegate.MODE_NIGHT_NO) themeToggle.setImageResource(R.drawable.ic_theme_dark);
themeToggle.setImageResource(R.drawable.ic_theme_dark); else if (mode == AppCompatDelegate.MODE_NIGHT_YES) themeToggle.setImageResource(R.drawable.ic_theme_light);
} else if (mode == AppCompatDelegate.MODE_NIGHT_YES) { else themeToggle.setImageResource(R.drawable.ic_theme_system);
themeToggle.setImageResource(R.drawable.ic_theme_light);
} else {
themeToggle.setImageResource(R.drawable.ic_theme_system);
}
} }
private void toggleVisibility(View view, TextView label, String text) { private void toggleVisibility(View view, TextView label, String text) {
if (view.getVisibility() == View.GONE) { boolean isGone = view.getVisibility() == View.GONE;
view.setVisibility(View.VISIBLE); view.setVisibility(isGone ? View.VISIBLE : View.GONE);
label.setText("" + text); label.setText((isGone ? "" : "") + text);
} else {
view.setVisibility(View.GONE);
label.setText("" + text);
}
} }
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE); IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED); registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else { } else {
registerReceiver(logReceiver, filter); registerReceiver(logReceiver, filter);
@ -503,11 +470,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
try { try { unregisterReceiver(logReceiver); } catch (Exception e) {}
unregisterReceiver(logReceiver);
} catch (Exception e) {
// Ignore
}
stopLogSizeUpdates(); stopLogSizeUpdates();
} }
@ -522,13 +485,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
savePrefs(); savePrefs();
Toast.makeText(this, R.string.saved_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.saved_toast, Toast.LENGTH_SHORT).show();
addToLog(getString(R.string.settings_saved)); addToLog(getString(R.string.settings_saved));
} else if (view.getId() == R.id.control) { } else if (view.getId() == R.id.control) handleControlClick();
handleControlClick(); else if (view.getId() == R.id.watchdog_control) handleWatchdogClick();
} else if (view.getId() == R.id.watchdog_control) { else if (view.getId() == R.id.btn_clear_log) showResetLogConfirmation();
handleWatchdogClick(); else if (view.getId() == R.id.btn_copy_log) {
} else if (view.getId() == R.id.btn_clear_log) {
showResetLogConfirmation();
} else if (view.getId() == R.id.btn_copy_log) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString()); ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString());
if (clipboard != null) { if (clipboard != null) {
@ -543,8 +503,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
.setTitle(R.string.log_reset_confirm_title) .setTitle(R.string.log_reset_confirm_title)
.setMessage(R.string.log_reset_confirm_msg) .setMessage(R.string.log_reset_confirm_msg)
.setPositiveButton(R.string.reset_log, (dialog, which) -> resetLogFile()) .setPositiveButton(R.string.reset_log, (dialog, which) -> resetLogFile())
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null).show();
.show();
} }
private void resetLogFile() { private void resetLogFile() {
@ -553,8 +512,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
pw.print(""); pw.print("");
connectionLog.setText(""); connectionLog.setText("");
addToLog(getString(R.string.log_reset_user)); addToLog(getString(R.string.log_reset_user));
SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE); getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE).edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
internalPrefs.edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
if (logWarning != null) logWarning.setVisibility(View.GONE); if (logWarning != null) logWarning.setVisibility(View.GONE);
updateLogSizeUI(); updateLogSizeUI();
Toast.makeText(this, R.string.log_cleared_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.log_cleared_toast, Toast.LENGTH_SHORT).show();
@ -564,12 +522,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
private void handleWatchdogClick() { private void handleWatchdogClick() {
boolean isEnabled = prefs.getWatchdogEnable(); if (prefs.getWatchdogEnable()) showWatchdogBiometricPrompt();
if (isEnabled) { else toggleWatchdog(false);
showWatchdogBiometricPrompt();
} else {
toggleWatchdog(false);
}
} }
private void toggleWatchdog(boolean stop) { private void toggleWatchdog(boolean stop) {
@ -579,35 +533,24 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
stopService(intent); stopService(intent);
addToLog(getString(R.string.watchdog_stopped)); addToLog(getString(R.string.watchdog_stopped));
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(intent.setAction(WatchdogService.ACTION_START));
startForegroundService(intent.setAction(WatchdogService.ACTION_START)); else startService(intent.setAction(WatchdogService.ACTION_START));
} else {
startService(intent.setAction(WatchdogService.ACTION_START));
}
addToLog(getString(R.string.watchdog_started)); addToLog(getString(R.string.watchdog_started));
} }
updateUI(); updateUI();
} }
private void handleControlClick() { private void handleControlClick() {
boolean isCurrentlyEnabled = prefs.getEnable(); if (prefs.getEnable()) showBiometricPrompt();
if (isCurrentlyEnabled) { else {
showBiometricPrompt(); BiometricManager bm = BiometricManager.from(this);
} else { int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
BiometricManager biometricManager = BiometricManager.from(this); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
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); android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (km != null && km.isDeviceSecure()) isSecure = true; if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) {
if (biometricManager.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS || isSecure) {
addToLog(getString(R.string.user_initiated_conn)); addToLog(getString(R.string.user_initiated_conn));
toggleService(false); toggleService(false);
} else { } else showEnrollmentDialog();
showEnrollmentDialog();
}
} }
} }
@ -624,9 +567,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
private void showBiometricPrompt() { private void showBiometricPrompt() {
Executor executor = ContextCompat.getMainExecutor(this); Executor ex = ContextCompat.getMainExecutor(this);
BiometricPrompt biometricPrompt = new BiometricPrompt(MainActivity.this, BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
executor, new BiometricPrompt.AuthenticationCallback() {
@Override @Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result); super.onAuthenticationSucceeded(result);
@ -634,45 +576,30 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
toggleService(true); toggleService(true);
} }
}); });
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.auth_required_title)).setSubtitle(getString(R.string.auth_required_subtitle)).setAllowedAuthenticators(auth).build());
.setTitle(getString(R.string.auth_required_title))
.setSubtitle(getString(R.string.auth_required_subtitle))
.setAllowedAuthenticators(authenticators)
.build();
biometricPrompt.authenticate(promptInfo);
} }
private void showWatchdogBiometricPrompt() { private void showWatchdogBiometricPrompt() {
Executor executor = ContextCompat.getMainExecutor(this); Executor ex = ContextCompat.getMainExecutor(this);
BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
@Override @Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result); super.onAuthenticationSucceeded(result);
toggleWatchdog(true); toggleWatchdog(true);
} }
}); });
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.unlock_watchdog_title)).setSubtitle(getString(R.string.unlock_watchdog_subtitle)).setAllowedAuthenticators(auth).build());
.setTitle(getString(R.string.unlock_watchdog_title))
.setSubtitle(getString(R.string.unlock_watchdog_subtitle))
.setAllowedAuthenticators(authenticators)
.build();
biometricPrompt.authenticate(promptInfo);
} }
private void toggleService(boolean isEnable) { private void toggleService(boolean stop) {
prefs.setEnable(!isEnable); prefs.setEnable(!stop);
savePrefs(); savePrefs();
updateUI(); updateUI();
Intent intent = new Intent(this, TProxyService.class); Intent intent = new Intent(this, TProxyService.class);
if (isEnable) { startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT));
startService(intent.setAction(TProxyService.ACTION_DISCONNECT)); addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting));
addToLog(getString(R.string.vpn_stopping));
} else {
startService(intent.setAction(TProxyService.ACTION_CONNECT));
addToLog(getString(R.string.vpn_starting));
}
} }
private void updateUI() { private void updateUI() {
@ -739,9 +666,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void scrollToBottom() { private void scrollToBottom() {
if (connectionLog.getLayout() != null) { if (connectionLog.getLayout() != null) {
final int scrollAmount = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight(); int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight();
if (scrollAmount > 0) if (scroll > 0) connectionLog.scrollTo(0, scroll);
connectionLog.scrollTo(0, scrollAmount);
} }
} }
} }

View File

@ -8,11 +8,16 @@ import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import java.util.Locale;
public class WatchdogService extends Service { public class WatchdogService extends Service {
private static final String CHANNEL_ID = "watchdog_channel"; private static final String CHANNEL_ID = "watchdog_channel";
@ -22,7 +27,9 @@ public class WatchdogService extends Service {
public static final String ACTION_STOP = "org.iiab.controller.WATCHDOG_STOP"; public static final String ACTION_STOP = "org.iiab.controller.WATCHDOG_STOP";
public static final String ACTION_HEARTBEAT = "org.iiab.controller.HEARTBEAT"; public static final String ACTION_HEARTBEAT = "org.iiab.controller.HEARTBEAT";
private static final int HEARTBEAT_INTERVAL_MS = 20 * 1000; private PowerManager.WakeLock wakeLock;
private WifiManager.WifiLock wifiLock;
private long lastPulseTime = 0;
@Override @Override
public void onCreate() { public void onCreate() {
@ -40,65 +47,85 @@ public class WatchdogService extends Service {
stopWatchdog(); stopWatchdog();
return START_NOT_STICKY; return START_NOT_STICKY;
} else if (ACTION_HEARTBEAT.equals(action)) { } else if (ACTION_HEARTBEAT.equals(action)) {
IIABWatchdog.performHeartbeat(this); performPulse();
// CRITICAL: Reschedule for the next pulse to create an infinite loop
scheduleHeartbeat(); scheduleHeartbeat();
} }
} }
return START_STICKY; return START_STICKY;
} }
private void startWatchdog() { private void performPulse() {
Notification notification = createNotification(); long now = System.currentTimeMillis();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { long delta = (lastPulseTime == 0) ? 0 : (now - lastPulseTime);
startForeground(NOTIFICATION_ID, notification); lastPulseTime = now;
} else {
startForeground(NOTIFICATION_ID, notification); boolean isNetworkConnected = isNetworkConnected();
String logMessage = String.format(Locale.getDefault(), "Pulse: Stimulating Termux... [Delta: %dms] [Network: %s]",
delta, isNetworkConnected ? "OK" : "FAIL");
IIABWatchdog.writeToBlackBox(this, logMessage);
IIABWatchdog.sendStimulus(this);
} }
IIABWatchdog.logSessionStart(this); private boolean isNetworkConnected() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return false;
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return netInfo != null && netInfo.isConnected();
}
private void startWatchdog() {
acquireLocks();
startForeground(NOTIFICATION_ID, createNotification());
lastPulseTime = System.currentTimeMillis();
scheduleHeartbeat(); scheduleHeartbeat();
} }
private void stopWatchdog() { private void stopWatchdog() {
cancelHeartbeat(); cancelHeartbeat();
IIABWatchdog.logSessionStop(this); releaseLocks();
stopForeground(true); stopForeground(true);
stopSelf(); stopSelf();
} }
private PendingIntent getHeartbeatPendingIntent() { private void acquireLocks() {
Intent intent = new Intent(this, WatchdogService.class); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
intent.setAction(ACTION_HEARTBEAT); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "IIAB:WatchdogWakeLock");
int flags = PendingIntent.FLAG_UPDATE_CURRENT; wakeLock.acquire(10 * 60 * 1000L); // 10 minutes max lock
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE; WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "IIAB:WatchdogWifiLock");
wifiLock.acquire();
} }
return PendingIntent.getService(this, 0, intent, flags);
private void releaseLocks() {
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
if (wifiLock != null && wifiLock.isHeld()) wifiLock.release();
} }
private void scheduleHeartbeat() { private void scheduleHeartbeat() {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = getHeartbeatPendingIntent(); Intent intent = new Intent(this, WatchdogService.class);
intent.setAction(ACTION_HEARTBEAT);
int flags = PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, flags);
if (alarmManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// This wakes up the device even in Doze Mode alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 30000, pendingIntent);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + HEARTBEAT_INTERVAL_MS,
pendingIntent);
} else { } else {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 30000, pendingIntent);
SystemClock.elapsedRealtime() + HEARTBEAT_INTERVAL_MS, }
pendingIntent);
} }
} }
private void cancelHeartbeat() { private void cancelHeartbeat() {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = getHeartbeatPendingIntent(); Intent intent = new Intent(this, WatchdogService.class);
if (alarmManager != null) { intent.setAction(ACTION_HEARTBEAT);
alarmManager.cancel(pendingIntent); int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0;
} PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, flags);
if (alarmManager != null) alarmManager.cancel(pendingIntent);
} }
@Override @Override
@ -108,35 +135,21 @@ public class WatchdogService extends Service {
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) { return null; }
return null;
}
private void createNotificationChannel() { private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Watchdog Service", NotificationManager.IMPORTANCE_LOW);
CHANNEL_ID, "IIAB Watchdog Service", NotificationManager nm = getSystemService(NotificationManager.class);
NotificationManager.IMPORTANCE_HIGH if (nm != null) nm.createNotificationChannel(channel);
);
channel.setDescription("Ensures services remain active when screen is off.");
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
} }
} }
private Notification createNotification() { private Notification createNotification() {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE);
return new NotificationCompat.Builder(this, CHANNEL_ID) return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("IIAB Watchdog Active") .setContentTitle("IIAB Watchdog Active")
.setContentText("Protecting Termux environment...") .setContentText("Maintaining Termux environment...")
.setSmallIcon(android.R.drawable.ic_lock_idle_lock) .setSmallIcon(android.R.drawable.ic_lock_idle_lock)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setOngoing(true) .setOngoing(true)
.build(); .build();
} }

View File

@ -2,14 +2,13 @@
set -euo pipefail set -euo pipefail
# gen_simple_inplace.sh # gen_simple_inplace.sh
# Generate PEP 503 "simple" indexes in-place for existing directories: # Moves wheels from a pool to their corresponding PEP 503 directory and generates the index.html files.
# <SIMPLE_DIR>/<pkg>/index.html # Standardizes web permissions (755 dir, 644 files)
# Optionally generates:
# <SIMPLE_DIR>/index.html
die() { echo "ERROR: $*" >&2; exit 1; } die() { echo "ERROR: $*" >&2; exit 1; }
SIMPLE_DIR="" FINAL_REPO=""
W_POOL=""
ONLY_PKG="" ONLY_PKG=""
NO_TOP=0 NO_TOP=0
DO_VERIFY=0 DO_VERIFY=0
@ -17,7 +16,8 @@ VERIFY_ONLY=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--simple-dir) SIMPLE_DIR="${2:-}"; shift 2 ;; --final-repo|--simple-dir) FINAL_REPO="${2:-}"; shift 2 ;;
--w-pool) W_POOL="${2:-}"; shift 2 ;;
--pkg) ONLY_PKG="${2:-}"; shift 2 ;; --pkg) ONLY_PKG="${2:-}"; shift 2 ;;
--no-top) NO_TOP=1; shift ;; --no-top) NO_TOP=1; shift ;;
--verify) DO_VERIFY=1; shift ;; --verify) DO_VERIFY=1; shift ;;
@ -25,14 +25,17 @@ while [ $# -gt 0 ]; do
-h|--help) -h|--help)
cat <<'EOF' cat <<'EOF'
Usage: Usage:
./gen_simple_inplace.sh --simple-dir /var/www/.../simple ./simple_builder.sh --final-repo ~/simple/ --w-pool ~/wheel_pool/
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --pkg cffi --no-top ./simple_builder.sh --final-repo ~/simple/ --pkg cffi
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --verify ./simple_builder.sh --final-repo ~/simple/ --verify
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --verify-only
Options: Options:
--no-top Don't rewrite /simple/index.html --final-repo Path to the repository's root directory (e.g., ~/simple/)
--verify Verify that each <pkg>/index.html href exists and sha256 matches --w-pool Path to the folder containing new wheels to accommodate (e.g., ~/wheel_pool/)
--verify-only Verify only (do not regenerate any index.html) --pkg Update/Verify only a specific package
--no-top Do not regenerate the main /simple/index.html file
--verify Verify that the href attributes exist and the SHA256 attributes match
--verify-only Only verify (does not regenerate any index.html files or move wheels)
EOF EOF
exit 0 exit 0
;; ;;
@ -40,16 +43,40 @@ EOF
esac esac
done done
[ -n "$SIMPLE_DIR" ] || die "--simple-dir is required" [ -n "$FINAL_REPO" ] || die "--final-repo is required"
[ -d "$SIMPLE_DIR" ] || die "Not a directory: $SIMPLE_DIR" [ -d "$FINAL_REPO" ] || die "It is not a valid directory: $FINAL_REPO"
is_artifact() { # Strict PEP 503 normalization
case "$1" in normalize_pkg_name() {
*.whl|*.tar.gz|*.zip|*.tgz) return 0 ;; echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[-_.]+/-/g'
*) return 1 ;;
esac
} }
# 1. Process the Pool of Wheels
if [ "$VERIFY_ONLY" -eq 0 ] && [ -n "$W_POOL" ]; then
[ -d "$W_POOL" ] || die "The pool is not a valid directory: $W_POOL"
echo "=> Scanning pool of wheels in: $W_POOL"
while IFS= read -r -d '' whl; do
filename="$(basename "$whl")"
# The distribution name is everything that comes before the first hyphen (-)
raw_dist="${filename%%-*}"
norm_pkg="$(normalize_pkg_name "$raw_dist")"
dest_dir="$FINAL_REPO/$norm_pkg"
# Create the folder and ensure its web permissions
mkdir -p "$dest_dir"
chmod 755 "$dest_dir"
echo " -> Moving: $filename to the directory /$norm_pkg/"
mv -f "$whl" "$dest_dir/"
# Ensure that the newly moved file has web permissions
chmod 644 "$dest_dir/$filename"
done < <(find "$W_POOL" -maxdepth 1 -type f -name '*.whl' -print0)
echo "=> Moving complete."
fi
verify_pkg_index() { verify_pkg_index() {
local pkgdir="$1" local pkgdir="$1"
local pkgname="$2" local pkgname="$2"
@ -57,19 +84,17 @@ verify_pkg_index() {
local errs=0 local errs=0
if [ ! -f "$idx" ]; then if [ ! -f "$idx" ]; then
echo "VERIFY FAIL [$pkgname]: missing index.html" >&2 echo "VERIFY FAIL [$pkgname]: index.html is missing" >&2
return 1 return 1
fi fi
# Extract href targets like: filename#sha256=....
# We assume filenames don't contain quotes/spaces (true for wheels/sdists typically).
while IFS= read -r href; do while IFS= read -r href; do
[ -n "$href" ] || continue [ -n "$href" ] || continue
local file="${href%%#sha256=*}" local file="${href%%#sha256=*}"
local want="${href##*#sha256=}" local want="${href##*#sha256=}"
if [ ! -f "$pkgdir/$file" ]; then if [ ! -f "$pkgdir/$file" ]; then
echo "VERIFY FAIL [$pkgname]: missing file: $file" >&2 echo "VERIFY FAIL [$pkgname]: file is missing: $file" >&2
errs=$((errs+1)) errs=$((errs+1))
continue continue
fi fi
@ -78,12 +103,11 @@ verify_pkg_index() {
got="$(sha256sum "$pkgdir/$file" | awk '{print $1}')" got="$(sha256sum "$pkgdir/$file" | awk '{print $1}')"
if [ "$got" != "$want" ]; then if [ "$got" != "$want" ]; then
echo "VERIFY FAIL [$pkgname]: sha256 mismatch for $file" >&2 echo "VERIFY FAIL [$pkgname]: sha256 mismatch for $file" >&2
echo " want: $want" >&2 echo " expected: $want" >&2
echo " got: $got" >&2 echo " got: $got" >&2
errs=$((errs+1)) errs=$((errs+1))
fi fi
done < <( done < <(
# Pull href="..."; keep only those containing #sha256=
grep -oE 'href="[^"]+"' "$idx" \ grep -oE 'href="[^"]+"' "$idx" \
| sed -E 's/^href="(.*)"$/\1/' \ | sed -E 's/^href="(.*)"$/\1/' \
| grep -E '#sha256=' || true | grep -E '#sha256=' || true
@ -103,61 +127,72 @@ write_pkg_index() {
local pkgname="$2" local pkgname="$2"
local idx="$pkgdir/index.html" local idx="$pkgdir/index.html"
# Collect artifacts in that pkg dir # Ensure directory and existing wheels permissions prior to this script
mapfile -t files < <(find "$pkgdir" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' -o -name '*.zip' -o -name '*.tgz' \) -printf '%f\n' | sort) chmod 755 "$pkgdir"
find "$pkgdir" -maxdepth 1 -type f -name '*.whl' -exec chmod 644 {} +
# If no artifacts, skip (or you can still write an empty index if you want) mapfile -t files < <(find "$pkgdir" -maxdepth 1 -type f -name '*.whl' -printf '%f\n' | sort)
[ "${#files[@]}" -gt 0 ] || return 0 [ "${#files[@]}" -gt 0 ] || return 0
{ {
echo "<!doctype html>" echo "<!doctype html>"
echo "<html><head><meta charset=\"utf-8\"><title>${pkgname}</title></head><body>" echo "<html><head><meta charset=\"utf-8\"><title>${pkgname}</title></head><body>"
for bn in "${files[@]}"; do for bn in "${files[@]}"; do
# hash the file in place
sha="$(sha256sum "$pkgdir/$bn" | awk '{print $1}')" sha="$(sha256sum "$pkgdir/$bn" | awk '{print $1}')"
printf '<a href="%s#sha256=%s">%s</a><br/>\n' "$bn" "$sha" "$bn" printf '<a href="%s#sha256=%s">%s</a><br/>\n' "$bn" "$sha" "$bn"
done done
echo "</body></html>" echo "</body></html>"
} > "$idx" } > "$idx"
# Ensure generated index permissions
chmod 644 "$idx"
} }
# Determine package dirs # Determine package directories
pkg_dirs=() pkg_dirs=()
if [ -n "$ONLY_PKG" ]; then if [ -n "$ONLY_PKG" ]; then
[ -d "$SIMPLE_DIR/$ONLY_PKG" ] || die "Package dir not found: $SIMPLE_DIR/$ONLY_PKG" ONLY_PKG="$(normalize_pkg_name "$ONLY_PKG")"
pkg_dirs+=("$SIMPLE_DIR/$ONLY_PKG") [ -d "$FINAL_REPO/$ONLY_PKG" ] || die "Package directory not found: $FINAL_REPO/$ONLY_PKG"
pkg_dirs+=("$FINAL_REPO/$ONLY_PKG")
else else
# All subdirs except hidden ones
while IFS= read -r d; do while IFS= read -r d; do
pkg_dirs+=("$d") pkg_dirs+=("$d")
done < <(find "$SIMPLE_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort) done < <(find "$FINAL_REPO" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
fi fi
if [ "$VERIFY_ONLY" -eq 0 ]; then if [ "$VERIFY_ONLY" -eq 0 ]; then
# Generate per-package indexes echo "=> Generating indexes (index.html) and standardizing permissions..."
# Ensure root directory permissions
chmod 755 "$FINAL_REPO"
for d in "${pkg_dirs[@]}"; do for d in "${pkg_dirs[@]}"; do
pkg="$(basename "$d")" pkg="$(basename "$d")"
write_pkg_index "$d" "$pkg" write_pkg_index "$d" "$pkg"
done done
# Top index (optional) # Main Index
if [ "$NO_TOP" -eq 0 ] && [ -z "$ONLY_PKG" ]; then if [ "$NO_TOP" -eq 0 ] && [ -z "$ONLY_PKG" ]; then
top="$SIMPLE_DIR/index.html" top="$FINAL_REPO/index.html"
{ {
echo "<!doctype html>" echo "<!doctype html>"
echo "<html><head><meta charset=\"utf-8\"><title>Simple Index</title></head><body>" echo "<html><head><meta charset=\"utf-8\"><title>Simple Index</title></head><body>"
for d in "${pkg_dirs[@]}"; do for d in "${pkg_dirs[@]}"; do
pkg="$(basename "$d")" pkg="$(basename "$d")"
if find "$d" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' -o -name '*.zip' -o -name '*.tgz' \) | grep -q .; then if find "$d" -maxdepth 1 -type f -name '*.whl' | grep -q .; then
printf '<a href="./%s/">%s</a><br/>\n' "$pkg" "$pkg" printf '<a href="./%s/">%s</a><br/>\n' "$pkg" "$pkg"
fi fi
done done
echo "</body></html>" echo "</body></html>"
} > "$top" } > "$top"
# Main index permissions
chmod 644 "$top"
fi fi
fi fi
if [ "$DO_VERIFY" -eq 1 ]; then if [ "$DO_VERIFY" -eq 1 ]; then
echo "=> Verifying integrity..."
vfail=0 vfail=0
for d in "${pkg_dirs[@]}"; do for d in "${pkg_dirs[@]}"; do
pkg="$(basename "$d")" pkg="$(basename "$d")"
@ -166,12 +201,12 @@ if [ "$DO_VERIFY" -eq 1 ]; then
fi fi
done done
if [ "$vfail" -ne 0 ]; then if [ "$vfail" -ne 0 ]; then
die "Verification failed" die "Verification failed for one or more packages."
fi fi
fi fi
if [ "$VERIFY_ONLY" -eq 1 ]; then if [ "$VERIFY_ONLY" -eq 1 ]; then
echo "OK: verified indexes under: $SIMPLE_DIR" echo "=> OK: indexes verified in: $FINAL_REPO"
else else
echo "OK: indexes generated under: $SIMPLE_DIR" echo "=> OK: process finished successfully in: $FINAL_REPO"
fi fi