Compare commits
No commits in common. "77742cd2a28ff4548160ae157c638e3f19843959" and "3c1b920d428020028e5b848bde5ca1fec1aea23a" have entirely different histories.
77742cd2a2
...
3c1b920d42
|
|
@ -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-07T08:21:48.428800901Z">
|
<DropdownSelection timestamp="2026-03-07T04:28:01.557850134Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=69K7MB899PKJGQBI" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=a026a310" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
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>
|
||||||
|
|
@ -12,6 +13,7 @@
|
||||||
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"
|
||||||
|
|
@ -19,28 +21,34 @@
|
||||||
<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" android:value="VPN service"/>
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
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" android:value="Watchdog and Heartbeat"/>
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Watchdog and Heartbeat"/>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:enabled="true" android:name=".ServiceReceiver" android:exported="true">
|
<receiver android:enabled="true" android:name=".ServiceReceiver"
|
||||||
|
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" />
|
||||||
|
|
@ -54,6 +62,7 @@
|
||||||
<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"/>
|
||||||
|
|
@ -64,14 +73,13 @@
|
||||||
<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" tools:ignore="QueryAllPackagesPermission" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" android:minSdkVersion="34" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
|
android:minSdkVersion="34" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,12 @@ 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 -> {
|
||||||
|
|
@ -143,6 +145,7 @@ 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();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -208,6 +211,8 @@ 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));
|
||||||
|
|
@ -389,7 +394,9 @@ 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) -> openBatterySettings(manufacturer))
|
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> {
|
||||||
|
openBatterySettings(manufacturer);
|
||||||
|
})
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
@ -400,29 +407,34 @@ 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", "com.coloros.safecenter.permission.startup.StartupAppListActivity"));
|
intent.setComponent(new ComponentName("com.coloros.safecenter",
|
||||||
|
"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", "com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
|
intent.setComponent(new ComponentName("com.coloros.oppoguardelf",
|
||||||
|
"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", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
|
intent.setComponent(new ComponentName("com.miui.powerkeeper",
|
||||||
|
"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_APPLICATION_DETAILS_SETTINGS);
|
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
@ -430,9 +442,20 @@ 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 = (currentMode == AppCompatDelegate.MODE_NIGHT_NO) ? AppCompatDelegate.MODE_NIGHT_YES :
|
int nextMode;
|
||||||
(currentMode == AppCompatDelegate.MODE_NIGHT_YES) ? AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : AppCompatDelegate.MODE_NIGHT_NO;
|
|
||||||
sharedPref.edit().putInt("ui_mode", nextMode).apply();
|
if (currentMode == AppCompatDelegate.MODE_NIGHT_NO) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -445,22 +468,32 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateThemeToggleButton(int mode) {
|
private void updateThemeToggleButton(int mode) {
|
||||||
if (mode == AppCompatDelegate.MODE_NIGHT_NO) themeToggle.setImageResource(R.drawable.ic_theme_dark);
|
if (mode == AppCompatDelegate.MODE_NIGHT_NO) {
|
||||||
else if (mode == AppCompatDelegate.MODE_NIGHT_YES) themeToggle.setImageResource(R.drawable.ic_theme_light);
|
themeToggle.setImageResource(R.drawable.ic_theme_dark);
|
||||||
else themeToggle.setImageResource(R.drawable.ic_theme_system);
|
} else if (mode == AppCompatDelegate.MODE_NIGHT_YES) {
|
||||||
|
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) {
|
||||||
boolean isGone = view.getVisibility() == View.GONE;
|
if (view.getVisibility() == View.GONE) {
|
||||||
view.setVisibility(isGone ? View.VISIBLE : View.GONE);
|
view.setVisibility(View.VISIBLE);
|
||||||
label.setText((isGone ? "▼ " : "▶ ") + text);
|
label.setText("▼ " + 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.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);
|
registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
} else {
|
} else {
|
||||||
registerReceiver(logReceiver, filter);
|
registerReceiver(logReceiver, filter);
|
||||||
|
|
@ -470,7 +503,11 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
try { unregisterReceiver(logReceiver); } catch (Exception e) {}
|
try {
|
||||||
|
unregisterReceiver(logReceiver);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
stopLogSizeUpdates();
|
stopLogSizeUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -485,10 +522,13 @@ 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) handleControlClick();
|
} else if (view.getId() == R.id.control) {
|
||||||
else if (view.getId() == R.id.watchdog_control) handleWatchdogClick();
|
handleControlClick();
|
||||||
else if (view.getId() == R.id.btn_clear_log) showResetLogConfirmation();
|
} else if (view.getId() == R.id.watchdog_control) {
|
||||||
else if (view.getId() == R.id.btn_copy_log) {
|
handleWatchdogClick();
|
||||||
|
} 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) {
|
||||||
|
|
@ -503,7 +543,8 @@ 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).show();
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetLogFile() {
|
private void resetLogFile() {
|
||||||
|
|
@ -512,7 +553,8 @@ 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));
|
||||||
getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE).edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
|
SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
|
||||||
|
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();
|
||||||
|
|
@ -522,8 +564,12 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleWatchdogClick() {
|
private void handleWatchdogClick() {
|
||||||
if (prefs.getWatchdogEnable()) showWatchdogBiometricPrompt();
|
boolean isEnabled = prefs.getWatchdogEnable();
|
||||||
else toggleWatchdog(false);
|
if (isEnabled) {
|
||||||
|
showWatchdogBiometricPrompt();
|
||||||
|
} else {
|
||||||
|
toggleWatchdog(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleWatchdog(boolean stop) {
|
private void toggleWatchdog(boolean stop) {
|
||||||
|
|
@ -533,24 +579,35 @@ 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) startForegroundService(intent.setAction(WatchdogService.ACTION_START));
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
else startService(intent.setAction(WatchdogService.ACTION_START));
|
startForegroundService(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() {
|
||||||
if (prefs.getEnable()) showBiometricPrompt();
|
boolean isCurrentlyEnabled = prefs.getEnable();
|
||||||
else {
|
if (isCurrentlyEnabled) {
|
||||||
BiometricManager bm = BiometricManager.from(this);
|
showBiometricPrompt();
|
||||||
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
} else {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
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);
|
android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||||
if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) {
|
if (km != null && km.isDeviceSecure()) isSecure = true;
|
||||||
|
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 showEnrollmentDialog();
|
} else {
|
||||||
|
showEnrollmentDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,8 +624,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showBiometricPrompt() {
|
private void showBiometricPrompt() {
|
||||||
Executor ex = ContextCompat.getMainExecutor(this);
|
Executor executor = ContextCompat.getMainExecutor(this);
|
||||||
BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
|
BiometricPrompt biometricPrompt = new BiometricPrompt(MainActivity.this,
|
||||||
|
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);
|
||||||
|
|
@ -576,30 +634,45 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
toggleService(true);
|
toggleService(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
||||||
bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.auth_required_title)).setSubtitle(getString(R.string.auth_required_subtitle)).setAllowedAuthenticators(auth).build());
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showWatchdogBiometricPrompt() {
|
private void showWatchdogBiometricPrompt() {
|
||||||
Executor ex = ContextCompat.getMainExecutor(this);
|
Executor executor = ContextCompat.getMainExecutor(this);
|
||||||
BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
|
BiometricPrompt biometricPrompt = new BiometricPrompt(this, 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);
|
||||||
toggleWatchdog(true);
|
toggleWatchdog(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
||||||
bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.unlock_watchdog_title)).setSubtitle(getString(R.string.unlock_watchdog_subtitle)).setAllowedAuthenticators(auth).build());
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleService(boolean stop) {
|
private void toggleService(boolean isEnable) {
|
||||||
prefs.setEnable(!stop);
|
prefs.setEnable(!isEnable);
|
||||||
savePrefs();
|
savePrefs();
|
||||||
updateUI();
|
updateUI();
|
||||||
Intent intent = new Intent(this, TProxyService.class);
|
Intent intent = new Intent(this, TProxyService.class);
|
||||||
startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT));
|
if (isEnable) {
|
||||||
addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting));
|
startService(intent.setAction(TProxyService.ACTION_DISCONNECT));
|
||||||
|
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() {
|
||||||
|
|
@ -666,8 +739,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
|
|
||||||
private void scrollToBottom() {
|
private void scrollToBottom() {
|
||||||
if (connectionLog.getLayout() != null) {
|
if (connectionLog.getLayout() != null) {
|
||||||
int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight();
|
final int scrollAmount = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight();
|
||||||
if (scroll > 0) connectionLog.scrollTo(0, scroll);
|
if (scrollAmount > 0)
|
||||||
|
connectionLog.scrollTo(0, scrollAmount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,11 @@ 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";
|
||||||
|
|
@ -27,9 +22,7 @@ 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 PowerManager.WakeLock wakeLock;
|
private static final int HEARTBEAT_INTERVAL_MS = 20 * 1000;
|
||||||
private WifiManager.WifiLock wifiLock;
|
|
||||||
private long lastPulseTime = 0;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
|
@ -47,85 +40,65 @@ 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)) {
|
||||||
performPulse();
|
IIABWatchdog.performHeartbeat(this);
|
||||||
|
// CRITICAL: Reschedule for the next pulse to create an infinite loop
|
||||||
scheduleHeartbeat();
|
scheduleHeartbeat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performPulse() {
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
long delta = (lastPulseTime == 0) ? 0 : (now - lastPulseTime);
|
|
||||||
lastPulseTime = now;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
private void startWatchdog() {
|
||||||
acquireLocks();
|
Notification notification = createNotification();
|
||||||
startForeground(NOTIFICATION_ID, createNotification());
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
lastPulseTime = System.currentTimeMillis();
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
IIABWatchdog.logSessionStart(this);
|
||||||
scheduleHeartbeat();
|
scheduleHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopWatchdog() {
|
private void stopWatchdog() {
|
||||||
cancelHeartbeat();
|
cancelHeartbeat();
|
||||||
releaseLocks();
|
IIABWatchdog.logSessionStop(this);
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
stopSelf();
|
stopSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acquireLocks() {
|
private PendingIntent getHeartbeatPendingIntent() {
|
||||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
Intent intent = new Intent(this, WatchdogService.class);
|
||||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "IIAB:WatchdogWakeLock");
|
intent.setAction(ACTION_HEARTBEAT);
|
||||||
wakeLock.acquire(10 * 60 * 1000L); // 10 minutes max lock
|
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||||
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);
|
||||||
Intent intent = new Intent(this, WatchdogService.class);
|
PendingIntent pendingIntent = getHeartbeatPendingIntent();
|
||||||
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,
|
||||||
} else {
|
SystemClock.elapsedRealtime() + HEARTBEAT_INTERVAL_MS,
|
||||||
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 30000, pendingIntent);
|
pendingIntent);
|
||||||
}
|
} else {
|
||||||
|
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||||
|
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);
|
||||||
Intent intent = new Intent(this, WatchdogService.class);
|
PendingIntent pendingIntent = getHeartbeatPendingIntent();
|
||||||
intent.setAction(ACTION_HEARTBEAT);
|
if (alarmManager != null) {
|
||||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0;
|
alarmManager.cancel(pendingIntent);
|
||||||
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, flags);
|
}
|
||||||
if (alarmManager != null) alarmManager.cancel(pendingIntent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -135,21 +108,35 @@ public class WatchdogService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) { return null; }
|
public IBinder onBind(Intent intent) {
|
||||||
|
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(CHANNEL_ID, "Watchdog Service", NotificationManager.IMPORTANCE_LOW);
|
NotificationChannel channel = new NotificationChannel(
|
||||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
CHANNEL_ID, "IIAB Watchdog Service",
|
||||||
if (nm != null) nm.createNotificationChannel(channel);
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
);
|
||||||
|
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("Maintaining Termux environment...")
|
.setContentText("Protecting 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# gen_simple_inplace.sh
|
# gen_simple_inplace.sh
|
||||||
# Moves wheels from a pool to their corresponding PEP 503 directory and generates the index.html files.
|
# Generate PEP 503 "simple" indexes in-place for existing directories:
|
||||||
# Standardizes web permissions (755 dir, 644 files)
|
# <SIMPLE_DIR>/<pkg>/index.html
|
||||||
|
# Optionally generates:
|
||||||
|
# <SIMPLE_DIR>/index.html
|
||||||
|
|
||||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
FINAL_REPO=""
|
SIMPLE_DIR=""
|
||||||
W_POOL=""
|
|
||||||
ONLY_PKG=""
|
ONLY_PKG=""
|
||||||
NO_TOP=0
|
NO_TOP=0
|
||||||
DO_VERIFY=0
|
DO_VERIFY=0
|
||||||
|
|
@ -16,8 +17,7 @@ VERIFY_ONLY=0
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--final-repo|--simple-dir) FINAL_REPO="${2:-}"; shift 2 ;;
|
--simple-dir) SIMPLE_DIR="${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,17 +25,14 @@ while [ $# -gt 0 ]; do
|
||||||
-h|--help)
|
-h|--help)
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
Usage:
|
Usage:
|
||||||
./simple_builder.sh --final-repo ~/simple/ --w-pool ~/wheel_pool/
|
./gen_simple_inplace.sh --simple-dir /var/www/.../simple
|
||||||
./simple_builder.sh --final-repo ~/simple/ --pkg cffi
|
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --pkg cffi --no-top
|
||||||
./simple_builder.sh --final-repo ~/simple/ --verify
|
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --verify
|
||||||
|
./gen_simple_inplace.sh --simple-dir /var/www/.../simple --verify-only
|
||||||
Options:
|
Options:
|
||||||
--final-repo Path to the repository's root directory (e.g., ~/simple/)
|
--no-top Don't rewrite /simple/index.html
|
||||||
--w-pool Path to the folder containing new wheels to accommodate (e.g., ~/wheel_pool/)
|
--verify Verify that each <pkg>/index.html href exists and sha256 matches
|
||||||
--pkg Update/Verify only a specific package
|
--verify-only Verify only (do not regenerate any index.html)
|
||||||
--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
|
||||||
;;
|
;;
|
||||||
|
|
@ -43,40 +40,16 @@ EOF
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[ -n "$FINAL_REPO" ] || die "--final-repo is required"
|
[ -n "$SIMPLE_DIR" ] || die "--simple-dir is required"
|
||||||
[ -d "$FINAL_REPO" ] || die "It is not a valid directory: $FINAL_REPO"
|
[ -d "$SIMPLE_DIR" ] || die "Not a directory: $SIMPLE_DIR"
|
||||||
|
|
||||||
# Strict PEP 503 normalization
|
is_artifact() {
|
||||||
normalize_pkg_name() {
|
case "$1" in
|
||||||
echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[-_.]+/-/g'
|
*.whl|*.tar.gz|*.zip|*.tgz) return 0 ;;
|
||||||
|
*) 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"
|
||||||
|
|
@ -84,17 +57,19 @@ verify_pkg_index() {
|
||||||
local errs=0
|
local errs=0
|
||||||
|
|
||||||
if [ ! -f "$idx" ]; then
|
if [ ! -f "$idx" ]; then
|
||||||
echo "VERIFY FAIL [$pkgname]: index.html is missing" >&2
|
echo "VERIFY FAIL [$pkgname]: missing index.html" >&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]: file is missing: $file" >&2
|
echo "VERIFY FAIL [$pkgname]: missing file: $file" >&2
|
||||||
errs=$((errs+1))
|
errs=$((errs+1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
@ -103,11 +78,12 @@ 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 " expected: $want" >&2
|
echo " want: $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
|
||||||
|
|
@ -127,72 +103,61 @@ write_pkg_index() {
|
||||||
local pkgname="$2"
|
local pkgname="$2"
|
||||||
local idx="$pkgdir/index.html"
|
local idx="$pkgdir/index.html"
|
||||||
|
|
||||||
# Ensure directory and existing wheels permissions prior to this script
|
# Collect artifacts in that pkg dir
|
||||||
chmod 755 "$pkgdir"
|
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)
|
||||||
find "$pkgdir" -maxdepth 1 -type f -name '*.whl' -exec chmod 644 {} +
|
|
||||||
|
|
||||||
mapfile -t files < <(find "$pkgdir" -maxdepth 1 -type f -name '*.whl' -printf '%f\n' | sort)
|
# If no artifacts, skip (or you can still write an empty index if you want)
|
||||||
[ "${#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 directories
|
# Determine package dirs
|
||||||
pkg_dirs=()
|
pkg_dirs=()
|
||||||
if [ -n "$ONLY_PKG" ]; then
|
if [ -n "$ONLY_PKG" ]; then
|
||||||
ONLY_PKG="$(normalize_pkg_name "$ONLY_PKG")"
|
[ -d "$SIMPLE_DIR/$ONLY_PKG" ] || die "Package dir not found: $SIMPLE_DIR/$ONLY_PKG"
|
||||||
[ -d "$FINAL_REPO/$ONLY_PKG" ] || die "Package directory not found: $FINAL_REPO/$ONLY_PKG"
|
pkg_dirs+=("$SIMPLE_DIR/$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 "$FINAL_REPO" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
|
done < <(find "$SIMPLE_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$VERIFY_ONLY" -eq 0 ]; then
|
if [ "$VERIFY_ONLY" -eq 0 ]; then
|
||||||
echo "=> Generating indexes (index.html) and standardizing permissions..."
|
# Generate per-package indexes
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# Main Index
|
# Top index (optional)
|
||||||
if [ "$NO_TOP" -eq 0 ] && [ -z "$ONLY_PKG" ]; then
|
if [ "$NO_TOP" -eq 0 ] && [ -z "$ONLY_PKG" ]; then
|
||||||
top="$FINAL_REPO/index.html"
|
top="$SIMPLE_DIR/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' | grep -q .; then
|
if find "$d" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' -o -name '*.zip' -o -name '*.tgz' \) | 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")"
|
||||||
|
|
@ -201,12 +166,12 @@ if [ "$DO_VERIFY" -eq 1 ]; then
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ "$vfail" -ne 0 ]; then
|
if [ "$vfail" -ne 0 ]; then
|
||||||
die "Verification failed for one or more packages."
|
die "Verification failed"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$VERIFY_ONLY" -eq 1 ]; then
|
if [ "$VERIFY_ONLY" -eq 1 ]; then
|
||||||
echo "=> OK: indexes verified in: $FINAL_REPO"
|
echo "OK: verified indexes under: $SIMPLE_DIR"
|
||||||
else
|
else
|
||||||
echo "=> OK: process finished successfully in: $FINAL_REPO"
|
echo "OK: indexes generated under: $SIMPLE_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue