diff --git a/apk/controller/app/src/main/AndroidManifest.xml b/apk/controller/app/src/main/AndroidManifest.xml index 0a5fc31..c46df93 100644 --- a/apk/controller/app/src/main/AndroidManifest.xml +++ b/apk/controller/app/src/main/AndroidManifest.xml @@ -66,6 +66,7 @@ + diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java index 6bdb25d..d902e60 100644 --- a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java +++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -101,6 +101,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe private TextView logWarning; private TextView logSizeText; private ImageButton themeToggle; + private ImageButton btnSettings; private TextView versionFooter; private ProgressBar logProgress; @@ -134,6 +135,14 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Intercept launch and redirect to Setup Wizard if first time + SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE); + if (!internalPrefs.getBoolean("setup_complete", false)) { + startActivity(new Intent(this, SetupActivity.class)); + finish(); + return; + } + prefs = new Preferences(this); setContentView(R.layout.main); @@ -200,6 +209,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe logWarning = findViewById(R.id.log_warning_text); logSizeText = findViewById(R.id.log_size_text); themeToggle = findViewById(R.id.theme_toggle); + btnSettings = findViewById(R.id.btn_settings); versionFooter = findViewById(R.id.version_text); configLayout = findViewById(R.id.config_layout); configLabel = findViewById(R.id.config_label); @@ -212,6 +222,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe btnClearLog.setOnClickListener(this); btnCopyLog.setOnClickListener(this); themeToggle.setOnClickListener(v -> toggleTheme()); + btnSettings.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, SetupActivity.class))); configLabel.setOnClickListener(v -> handleConfigToggle()); advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, getString(R.string.advanced_settings_label))); logLabel.setOnClickListener(v -> handleLogToggle()); @@ -241,7 +252,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe applySavedTheme(); setVersionFooter(); updateUI(); - initiatePermissionChain(); addToLog(getString(R.string.app_started)); diff --git a/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java new file mode 100644 index 0000000..6ea6d4a --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java @@ -0,0 +1,179 @@ +package org.iiab.controller; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.net.VpnService; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.widget.Button; +import android.widget.TextView; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; + +public class SetupActivity extends AppCompatActivity { + + private static final String TERMUX_PERMISSION = "com.termux.permission.RUN_COMMAND"; + + private SwitchCompat switchNotif, switchTermux, switchVpn, switchBattery; + private Button btnContinue; + + private ActivityResultLauncher requestPermissionLauncher; + private ActivityResultLauncher vpnLauncher; + private ActivityResultLauncher batteryLauncher; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_setup); + + TextView welcomeText = findViewById(R.id.setup_welcome_text); + welcomeText.setText(getString(R.string.setup_welcome, getString(R.string.app_name))); + + switchNotif = findViewById(R.id.switch_perm_notifications); + switchTermux = findViewById(R.id.switch_perm_termux); + switchVpn = findViewById(R.id.switch_perm_vpn); + switchBattery = findViewById(R.id.switch_perm_battery); + btnContinue = findViewById(R.id.btn_setup_continue); + + // Hide Notification switch if Android < 13 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + switchNotif.setVisibility(android.view.View.GONE); + } + + setupLaunchers(); + setupListeners(); + checkAllPermissions(); + + btnContinue.setOnClickListener(v -> { + // Save flag so we don't show this screen again + SharedPreferences prefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE); + prefs.edit().putBoolean("setup_complete", true).apply(); + + finish(); + }); + } + + private void setupLaunchers() { + requestPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + isGranted -> checkAllPermissions() + ); + + vpnLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> checkAllPermissions() + ); + + batteryLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> checkAllPermissions() + ); + } + + private void setupListeners() { + switchNotif.setOnClickListener(v -> { + if (switchNotif.isChecked()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + switchNotif.setChecked(hasNotifPermission()); // Revert visual state pending system response + }); + + switchTermux.setOnClickListener(v -> { + if (switchTermux.isChecked()) { + requestPermissionLauncher.launch(TERMUX_PERMISSION); + } + switchTermux.setChecked(hasTermuxPermission()); + }); + + switchVpn.setOnClickListener(v -> { + if (switchVpn.isChecked()) { + Intent intent = VpnService.prepare(this); + if (intent != null) { + vpnLauncher.launch(intent); + } else { + checkAllPermissions(); // Already granted + } + } + switchVpn.setChecked(hasVpnPermission()); + }); + + switchBattery.setOnClickListener(v -> { + if (switchBattery.isChecked()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + getPackageName())); + batteryLauncher.launch(intent); + } + } + switchBattery.setChecked(hasBatteryPermission()); + }); + } + + @Override + protected void onResume() { + super.onResume(); + checkAllPermissions(); // Refresh state if user returns from settings + } + + private void checkAllPermissions() { + boolean notif = hasNotifPermission(); + boolean termux = hasTermuxPermission(); + boolean vpn = hasVpnPermission(); + boolean battery = hasBatteryPermission(); + + switchNotif.setChecked(notif); + switchTermux.setChecked(termux); + switchVpn.setChecked(vpn); + switchBattery.setChecked(battery); + + // Lock switches if already granted to prevent user confusion + if (notif) switchNotif.setEnabled(false); + if (termux) switchTermux.setEnabled(false); + if (vpn) switchVpn.setEnabled(false); + if (battery) switchBattery.setEnabled(false); + + boolean allGranted = termux && vpn && battery; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + allGranted = allGranted && notif; + } + + btnContinue.setEnabled(allGranted); + btnContinue.setBackgroundTintList(ContextCompat.getColorStateList(this, + allGranted ? R.color.btn_explore_ready : R.color.btn_explore_disabled)); + } + + private boolean hasNotifPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + private boolean hasTermuxPermission() { + return ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasVpnPermission() { + return VpnService.prepare(this) == null; + } + + private boolean hasBatteryPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm != null && pm.isIgnoringBatteryOptimizations(getPackageName()); + } + return true; + } +} \ No newline at end of file diff --git a/apk/controller/app/src/main/res/layout/activity_setup.xml b/apk/controller/app/src/main/res/layout/activity_setup.xml new file mode 100644 index 0000000..7dfa601 --- /dev/null +++ b/apk/controller/app/src/main/res/layout/activity_setup.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + +