diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml
index 12278a572..17c1af7c8 100644
--- a/controller/app/src/main/AndroidManifest.xml
+++ b/controller/app/src/main/AndroidManifest.xml
@@ -61,19 +61,12 @@
-
-
-
+
MASTER_ROSTER = java.util.Arrays.asList(
- new IiabModule("books", R.string.dash_books, false),
- new IiabModule("code", R.string.dash_code, false),
- new IiabModule("kiwix", R.string.dash_kiwix, true),
- new IiabModule("kolibri", R.string.dash_kolibri, false),
- new IiabModule("maps", R.string.dash_maps, false),
- new IiabModule("matomo", R.string.dash_matomo, false),
- new IiabModule("dashboard", R.string.dash_system, false)
- );
-
- // ADFA-5192: SystemState is now a top-level enum (org.iiab.controller.SystemState),
- // extracted so it survives the retirement of this legacy fragment.
- private SystemState currentSystemState = SystemState.NONE;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_dashboard, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- org.iiab.controller.help.TooltipWiring.wireAll(view);
-
- // Bindings
- txtDeviceName = view.findViewById(R.id.dash_text_device_name);
- txtAndroidVersion = view.findViewById(R.id.dash_text_android_version);
- txtHostArch = view.findViewById(R.id.dash_text_host_arch);
-
- // --- DIAGNOSTIC BYPASS (AGGRESSIVE ALERT DIALOG) ---
- txtDeviceName.setOnClickListener(v -> {
- File iiabDir = new File(requireContext().getFilesDir(), "rootfs/installed-rootfs/iiab");
- String message;
-
- if (!iiabDir.exists()) {
- message = getString(R.string.dash_diag_dir_missing, iiabDir.getAbsolutePath());
- } else {
- String[] contents = iiabDir.list();
- if (contents == null || contents.length == 0) {
- message = getString(R.string.dash_diag_dir_empty);
- } else {
- message = getString(R.string.dash_diag_dir_contains, contents.length, java.util.Arrays.toString(contents));
- }
- }
-
- // Force a blocking UI dialog to show the results
- new BrandDialog(requireContext())
- .setTitle(R.string.dash_diag_title)
- .setMessage(message)
- .setPositive(R.string.dash_diag_btn_ok, null)
- .show();
- });
- // -------------------------
-
- txtWifiIp = view.findViewById(R.id.dash_text_wifi_ip);
- txtHotspotIp = view.findViewById(R.id.dash_text_hotspot_ip);
- txtUptime = view.findViewById(R.id.dash_text_uptime);
- badgeStatus = view.findViewById(R.id.dash_badge_status);
-
- gaugeStorage = view.findViewById(R.id.gauge_storage);
- gaugeRam = view.findViewById(R.id.gauge_ram);
- gaugeSwap = view.findViewById(R.id.gauge_swap);
- gaugeBattery = view.findViewById(R.id.gauge_battery);
- gaugeFlipper = view.findViewById(R.id.gauge_flipper);
- btnFlipGauges = view.findViewById(R.id.btn_flip_gauges);
- gaugesContainer = view.findViewById(R.id.dash_gauges_container);
-
- // --- ANIMATION SETUP FOR VIEWFLIPPER ---
- // Sets sliding animations for a smooth page transition
- gaugeFlipper.setInAnimation(requireContext(), android.R.anim.slide_in_left);
- gaugeFlipper.setOutAnimation(requireContext(), android.R.anim.slide_out_right);
-
- btnFlipGauges.setOnClickListener(v -> {
- gaugeFlipper.showNext();
- // Trigger gauge animation for whichever page just became visible
- triggerVisibleGaugesAnimation();
- });
-
- // Trigger animation when touching the gauges directly
- View.OnClickListener animateCurrentClick = v -> triggerVisibleGaugesAnimation();
- gaugeFlipper.setOnClickListener(animateCurrentClick);
- gaugeStorage.setOnClickListener(animateCurrentClick);
- gaugeBattery.setOnClickListener(animateCurrentClick);
- gaugeRam.setOnClickListener(animateCurrentClick);
- gaugeSwap.setOnClickListener(animateCurrentClick);
-
- ledTermuxState = view.findViewById(R.id.led_termux_state);
- txtTermuxState = view.findViewById(R.id.text_termux_state);
- txtTermuxArch = view.findViewById(R.id.dash_text_termux_arch);
- txtDebianArch = view.findViewById(R.id.dash_text_debian_arch);
- archContainer = view.findViewById(R.id.dash_arch_wrapper);
- modulesContainer = view.findViewById(R.id.modules_container);
- modulesTitle = view.findViewById(R.id.dash_modules_title);
-
- modulesContainer.setVisibility(View.GONE);
- modulesTitle.setText(String.format(getString(R.string.label_separator_up), getString(R.string.dash_installed_modules)));
-
- // Listener to collapse/expand
- modulesTitle.setOnClickListener(v -> {
- boolean isGone = modulesContainer.getVisibility() == View.GONE;
- modulesContainer.setVisibility(isGone ? View.VISIBLE : View.GONE);
- modulesTitle.setText(String.format(getString(isGone ? R.string.label_separator_down : R.string.label_separator_up), getString(R.string.dash_installed_modules)));
- });
-
- // Generate module views dynamically
- createModuleViews();
-
- // Configure refresh timer (every 5 seconds) -- local device stats only;
- // network server/module status is polled off the main thread by the VM below.
- refreshRunnable = new Runnable() {
- @Override
- public void run() {
- updateSystemStats();
- refreshHandler.postDelayed(this, 5000);
- }
- };
-
- // Network status: polled off the main thread on the shared scheduler and
- // observed here (ADFA-4457). Replaces the per-tick `new Thread()` ping.
- statusViewModel = new ViewModelProvider(this, new DashboardStatusViewModelFactory())
- .get(DashboardStatusViewModel.class);
- statusViewModel.state().observe(getViewLifecycleOwner(), this::renderStatus);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- refreshHandler.post(refreshRunnable);
- statusViewModel.start(collectModuleEndpoints());
- // TRIGGER ANIMATION WHEN ENTERING TAB
- new Handler(Looper.getMainLooper()).postDelayed(this::triggerVisibleGaugesAnimation, 100);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- refreshHandler.removeCallbacks(refreshRunnable);
- statusViewModel.stop();
- }
-
- private void updateSystemStats() {
- txtDeviceName.setText(getDeviceName());
-
- // --- FETCH AND DISPLAY BASE ANDROID VERSION ---
- String androidRelease = android.os.Build.VERSION.RELEASE;
- int sdkVersion = android.os.Build.VERSION.SDK_INT;
- txtAndroidVersion.setText(getString(R.string.dash_android_version_value, "v" + androidRelease, String.valueOf(sdkVersion)));
-
- // --- FETCH AND DISPLAY HOST (DEVICE) ARCHITECTURE ---
- // This must be the REAL device arch, not the app's ABI: a 32-bit app can
- // run on a 64-bit device (used for testing the 32-bit path), and the
- // device panel must still report 64-bit. App/content arch keeps using
- // getTermuxArch() elsewhere (modules, termux, debian).
- if (txtHostArch != null) {
- String deviceArch = new GetDeviceArchUseCase(new BuildDeviceAbiProvider()).execute();
- txtHostArch.setText(deviceArch);
- }
-
- // --- CALCULATE SERVER UPTIME ---
- long uptimeMillis = android.os.SystemClock.elapsedRealtime();
- long minutes = (uptimeMillis / (1000 * 60)) % 60;
- long hours = (uptimeMillis / (1000 * 60 * 60)) % 24;
- long days = (uptimeMillis / (1000 * 60 * 60 * 24));
-
- // Format: "Uptime: 2d 14h 05m" (Omit days if 0)
- String timeStr = (days > 0) ?
- getString(R.string.dash_format_uptime_days, days, hours, minutes) :
- getString(R.string.dash_format_uptime_hours, hours, minutes);
-
- txtUptime.setText(timeStr);
- txtWifiIp.setText(getWifiIp());
- txtHotspotIp.setText(getHotspotIp());
-
- // --- GET REAL RAM AND SWAP FROM LINUX ---
- long memTotal = 0, memAvailable = 0, swapTotal = 0, swapFree = 0;
- try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) {
- String line;
- while ((line = br.readLine()) != null) {
- if (line.startsWith("MemTotal:")) memTotal = parseMemLine(line);
- else if (line.startsWith("MemAvailable:")) memAvailable = parseMemLine(line);
- // If phone is old and doesn't have "MemAvailable", use "MemFree"
- else if (memAvailable == 0 && line.startsWith("MemFree:")) memAvailable = parseMemLine(line);
- else if (line.startsWith("SwapTotal:")) swapTotal = parseMemLine(line);
- else if (line.startsWith("SwapFree:")) swapFree = parseMemLine(line);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- // Convert the values from kB to GB (1 GB = 1048576 kB)
- double memTotalGb = memTotal / 1048576.0;
- double memUsedGb = (memTotal - memAvailable) / 1048576.0;
- int memProgress = memTotal > 0 ? (int) (((memTotal - memAvailable) * 100) / memTotal) : 0;
-
- double swapTotalGb = swapTotal / 1048576.0;
- double swapUsedGb = (swapTotal - swapFree) / 1048576.0;
- int swapProgress = swapTotal > 0 ? (int) (((swapTotal - swapFree) * 100) / swapTotal) : 0;
-
- File path = android.os.Environment.getDataDirectory();
- long totalSpace = path.getTotalSpace() / (1024 * 1024 * 1024);
- long freeSpace = path.getFreeSpace() / (1024 * 1024 * 1024);
- long usedSpace = totalSpace - freeSpace;
-
- // --- UPDATE GAUGE VIEWS (ANIMATED AND COLORED) ---
- int baseColorRam = ContextCompat.getColor(requireContext(), R.color.dash_bar_ram);
- int baseColorSwap = ContextCompat.getColor(requireContext(), R.color.dash_bar_swap);
- int baseColorStorage = ContextCompat.getColor(requireContext(), R.color.dash_bar_storage);
-
- int warnColor = ContextCompat.getColor(requireContext(), R.color.status_warning); // Orange
- int dangerColor = ContextCompat.getColor(requireContext(), R.color.status_danger); // Red
-
- // RAM Gauge (Warning at 90%, Danger at 95%)
- int finalColorRam = memProgress >= 95 ? dangerColor : (memProgress >= 90 ? warnColor : baseColorRam);
- String strRam = getString(R.string.dash_format_gb, memUsedGb, memTotalGb);
- if (gaugeRam != null)
- gaugeRam.updateData(memProgress, strRam, getString(R.string.dash_ram_memory), finalColorRam);
-
- // SWAP Gauge (Warning at 90%, Danger at 95%)
- if (gaugeSwap != null) {
- if (swapTotal > 0) {
- int finalColorSwap = swapProgress >= 95 ? dangerColor : (swapProgress >= 90 ? warnColor : baseColorSwap);
- String strSwap = getString(R.string.dash_format_gb, swapUsedGb, swapTotalGb);
- gaugeSwap.updateData(swapProgress, strSwap, getString(R.string.dash_swap_virtual), finalColorSwap);
- } else {
- gaugeSwap.updateData(0, getString(R.string.dash_format_na), getString(R.string.dash_swap_virtual), baseColorSwap);
- }
- }
-
- // STORAGE Gauge (Warning at 90%, Danger at 95%)
- if (gaugeStorage != null) {
- int storageProgress = totalSpace > 0 ? (int) ((usedSpace * 100f) / totalSpace) : 0;
- int finalColorStorage = storageProgress >= 95 ? dangerColor : (storageProgress >= 90 ? warnColor : baseColorStorage);
- String strStorage = getString(R.string.dash_format_gb_int, usedSpace, totalSpace);
- gaugeStorage.updateData(storageProgress, strStorage, getString(R.string.dash_main_storage), finalColorStorage);
- }
-
- // --- BATTERY GAUGE LOGIC ---
- if (gaugeBattery != null) {
- int batLevel = -1;
- boolean isCharging = false;
-
- try {
- IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- Intent batteryStatus = requireContext().registerReceiver(null, iFilter);
-
- if (batteryStatus != null) {
- int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
- int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
-
- // Ensure percentage is always between 0 and 100
- if (level != -1 && scale != -1) {
- batLevel = (int) ((level / (float) scale) * 100f);
- }
-
- // Strict check to see if it's connected to power (AC, USB, or Wireless)
- int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
- isCharging = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB ||
- chargePlug == BatteryManager.BATTERY_PLUGGED_AC ||
- chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
- }
- } catch (Exception e) {
- android.util.Log.e("IIAB-Dashboard", "Error reading battery stats", e);
- }
-
- // Apply exact requested battery colors
- int colorBattery;
- if (batLevel <= 33) {
- colorBattery = warnColor; // Orange (1-33%)
- } else if (batLevel <= 66) {
- colorBattery = ContextCompat.getColor(requireContext(), R.color.status_success); // Green (34-66%)
- } else {
- colorBattery = ContextCompat.getColor(requireContext(), R.color.status_info); // Blue (67-100%)
- }
-
- // Update the gauge with the newly assigned 4-parameter method
- if (batLevel >= 0) {
- // Only add the lightning bolt if isCharging is true
- String batStr = isCharging ? getString(R.string.dash_format_pct_charging, batLevel) : getString(R.string.dash_format_pct, batLevel);
- gaugeBattery.updateData(batLevel, batStr, getString(R.string.dash_battery_title), colorBattery);
- } else {
- // Default fallback if we can't read the battery
- colorBattery = ContextCompat.getColor(requireContext(), R.color.dash_text_secondary);
- gaugeBattery.updateData(0, getString(R.string.dash_format_pct_na), getString(R.string.dash_battery_title), colorBattery);
- }
- }
- }
-
- // Triggers the fill animation only for the gauges currently displayed on screen
- private void triggerVisibleGaugesAnimation() {
- if (gaugeFlipper.getDisplayedChild() == 0) {
- if (gaugeStorage != null) gaugeStorage.triggerAnimation();
- if (gaugeBattery != null) gaugeBattery.triggerAnimation();
- } else {
- if (gaugeRam != null) gaugeRam.triggerAnimation();
- if (gaugeSwap != null) gaugeSwap.triggerAnimation();
- }
- }
-
- private void createModuleViews() {
- modulesContainer.removeAllViews();
-
- // Determine if the installed Termux is 64-bit
- String arch = getTermuxArch();
- boolean is64Bit = arch != null && arch.contains("64");
-
- // Filter the Master Roster based on architecture support
- java.util.List activeModules = new java.util.ArrayList<>();
- for (IiabModule module : MASTER_ROSTER) {
- if (module.requires64Bit && !is64Bit) {
- continue;
- }
- activeModules.add(module);
- }
-
- // Build the UI grid dynamically using the filtered list
- int numCols = 3;
- int numRows = (int) Math.ceil((double) activeModules.size() / numCols);
-
- for (int row = 0; row < numRows; row++) {
- LinearLayout rowLayout = new LinearLayout(requireContext());
- rowLayout.setOrientation(LinearLayout.HORIZONTAL);
- rowLayout.setLayoutParams(new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- rowLayout.setBaselineAligned(false);
- rowLayout.setWeightSum(numCols);
- rowLayout.setPadding(0, 0, 0, 16);
-
- for (int col = 0; col < numCols; col++) {
- int index = (row * numCols) + col;
-
- LinearLayout cell = new LinearLayout(requireContext());
- LinearLayout.LayoutParams cellParams = new LinearLayout.LayoutParams(
- 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
-
- // Margins to prevent them from sticking together
- int margin = 8;
- if (col == 0) cellParams.setMargins(0, 0, margin, 0); // Left
- else if (col == 1) cellParams.setMargins(margin / 2, 0, margin / 2, 0); // Center
- else cellParams.setMargins(margin, 0, 0, 0); // Right
-
- cell.setLayoutParams(cellParams);
-
- if (index < activeModules.size()) {
- IiabModule currentMod = activeModules.get(index);
-
- cell.setOrientation(LinearLayout.HORIZONTAL);
- cell.setBackgroundResource(R.drawable.rounded_button);
- cell.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
- androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_module_bg)));
- cell.setPadding(16, 24, 16, 24);
- cell.setGravity(android.view.Gravity.CENTER);
-
- View led = new View(requireContext());
- led.setLayoutParams(new LinearLayout.LayoutParams(20, 20));
- led.setBackgroundResource(R.drawable.led_off);
- led.setId(View.generateViewId());
-
- TextView name = new TextView(requireContext());
- LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- textParams.setMargins(12, 0, 0, 0);
- name.setLayoutParams(textParams);
-
- // Inject the data from the Master Roster
- name.setText(getString(currentMod.nameResId));
- name.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_module_text));
- name.setTextSize(11f);
- name.setSingleLine(true);
-
- cell.addView(led);
- cell.addView(name);
-
- // The background ping thread relies on this tag to check the URL!
- cell.setTag(currentMod.endpoint);
- } else {
- cell.setVisibility(View.INVISIBLE);
- }
- rowLayout.addView(cell);
- }
- modulesContainer.addView(rowLayout);
- }
- }
-
- /** Renders the latest network status snapshot (main thread, from the ViewModel). */
- private void renderStatus(DashboardStatus status) {
- if (!isAdded() || getActivity() == null) return;
-
- boolean isMainServerAlive = status.serverAlive();
- currentSystemState = evaluateSystemState(isMainServerAlive);
-
- if (getActivity() instanceof MainActivity) {
- ((MainActivity) getActivity()).updateUIColorsAndVisibility();
- }
-
- if (archContainer != null) {
- if (isArchCalculated && currentSystemState != SystemState.NONE) {
- archContainer.setVisibility(View.VISIBLE);
- txtTermuxArch.setText(cachedTermuxArch);
- txtDebianArch.setText(cachedDebianArch);
- } else {
- archContainer.setVisibility(View.GONE);
- }
- }
-
- if (currentSystemState == SystemState.ONLINE) {
- badgeStatus.setText(R.string.dash_online);
- badgeStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
- androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_status_online)));
- } else {
- badgeStatus.setText(R.string.dash_offline);
- badgeStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
- androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_secondary)));
- }
-
- switch (currentSystemState) {
- case ONLINE:
- ledTermuxState.setBackgroundResource(R.drawable.led_on_green);
- txtTermuxState.setText(getString(R.string.dash_state_online));
- txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
- break;
- case OFFLINE:
- ledTermuxState.setBackgroundResource(R.drawable.led_off);
- txtTermuxState.setText(getString(R.string.dash_state_offline));
- txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_secondary));
- break;
- case DEBIAN_ONLY:
- ledTermuxState.setBackgroundResource(R.drawable.led_off);
- txtTermuxState.setText(getString(R.string.dash_state_debian_only));
- txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
- break;
- case INSTALLER:
- ledTermuxState.setBackgroundResource(R.drawable.led_off);
- txtTermuxState.setText(getString(R.string.dash_state_installer));
- txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
- break;
- case TERMUX_ONLY: // Fallthrough intended; no longer used
- case NONE:
- ledTermuxState.setBackgroundResource(R.drawable.led_off);
- txtTermuxState.setText(getString(R.string.dash_state_none));
- txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_warning));
- break;
- }
-
- for (int r = 0; r < modulesContainer.getChildCount(); r++) {
- LinearLayout row = (LinearLayout) modulesContainer.getChildAt(r);
- for (int c = 0; c < row.getChildCount(); c++) {
- LinearLayout card = (LinearLayout) row.getChildAt(c);
- String endpoint = (String) card.getTag();
- if (endpoint == null) continue;
- View led = card.getChildAt(0);
- boolean isModuleAlive = Boolean.TRUE.equals(status.moduleAlive().get(endpoint));
- led.setBackgroundResource(isModuleAlive ? R.drawable.led_on_green : R.drawable.led_off);
- }
- }
- }
-
- /** Endpoints of the currently-visible module cards (tags set in createModuleViews). */
- private List collectModuleEndpoints() {
- List endpoints = new ArrayList<>();
- if (modulesContainer == null) return endpoints;
- for (int r = 0; r < modulesContainer.getChildCount(); r++) {
- LinearLayout row = (LinearLayout) modulesContainer.getChildAt(r);
- for (int c = 0; c < row.getChildCount(); c++) {
- Object tag = row.getChildAt(c).getTag();
- if (tag instanceof String) endpoints.add((String) tag);
- }
- }
- return endpoints;
- }
-
- // Extracts the numbers (in kB) from the lines of /proc/meminfo
- private long parseMemLine(String line) {
- return SystemStatsUtil.parseMemLine(line);
- }
-
- // --- METHODS FOR OBTAINING IPs ---
- private String getWifiIp() {
- return getIpByInterface("wlan0");
- }
-
- private String getHotspotIp() {
- String[] hotspotInterfaces = {"ap0", "wlan1", "swlan0"};
- for (String iface : hotspotInterfaces) {
- String ip = getIpByInterface(iface);
- if (!ip.equals("--")) return ip;
- }
- return "--";
- }
-
- private String getIpByInterface(String interfaceName) {
- try {
- List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
- for (NetworkInterface intf : interfaces) {
- if (intf.getName().equalsIgnoreCase(interfaceName)) {
- List addrs = Collections.list(intf.getInetAddresses());
- for (InetAddress addr : addrs) {
- if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
- return addr.getHostAddress();
- }
- }
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return "--";
- }
-
- // --- METHODS FOR OBTAINING THE DEVICE NAME ---
- private String getDeviceName() {
- String manufacturer = android.os.Build.MANUFACTURER;
- String model = android.os.Build.MODEL;
-
- if (model.toLowerCase().startsWith(manufacturer.toLowerCase())) {
- return capitalize(model);
- } else {
- return capitalize(manufacturer) + " " + model;
- }
- }
-
- private String capitalize(String s) {
- if (s == null || s.length() == 0) return "";
- char first = s.charAt(0);
- if (Character.isUpperCase(first)) {
- return s;
- } else {
- return Character.toUpperCase(first) + s.substring(1);
- }
- }
-
- // --- MASTER STATE EVALUATOR (NATIVE WITH LEGACY MENTAL MAP) ---
- private SystemState evaluateSystemState(boolean isNginxAlive) {
-
- // 0. Calculate native architecture only once
- if (!isArchCalculated) {
- cachedTermuxArch = getTermuxArch();
- cachedDebianArch = getDebianArch(cachedTermuxArch);
- isArchCalculated = true;
- }
-
- // Setup paths for native direct inspection
- File rootfsDir = new File(requireContext().getFilesDir(), "rootfs/installed-rootfs/iiab");
- File debianBash = new File(rootfsDir, "bin/bash");
- File flagIiabReady = new File(rootfsDir, "usr/local/pdsm/flag_install_ready");
-
- // --- 1. Does Termux physically exist on the Android device? ---
- /*
- * [OBSOLETE IN NATIVE ARCHITECTURE]
- * Previously used PackageManager to check "com.termux" and verify firstInstallTime.
- * Handled "The Purge" (Ghost Handling) if signatures mismatched.
- * No longer needed because PRoot and Aria2 are compiled directly into this app.
- */
-
- // --- 2. Does the Nginx server respond? ---
- if (isNginxAlive) {
- return SystemState.ONLINE;
- }
-
- // --- 3. Is IIAB fully compiled/restored and ready? ---
- /*
- * [NATIVE ADAPTATION]
- * Previously looked for "flag_iiab_ready" in /sdcard/.iiab_state.
- */
- if (flagIiabReady.exists()) {
- return SystemState.OFFLINE;
- }
-
- // --- 4. Is the base Debian OS installed, but NO IIAB yet? ---
- /*
- * [NATIVE ADAPTATION]
- * Previously looked for "flag_system_installed" in /sdcard/.iiab_state.
- * Now directly checks if TarExtractor successfully unpacked the base Linux filesystem.
- */
- if (debianBash.exists()) {
- return SystemState.DEBIAN_ONLY;
- }
-
- // --- 5. Is only the installer ready? ---
- /*
- * [OBSOLETE IN NATIVE ARCHITECTURE]
- * Previously looked for "flag_installer_present" to know if the Bash script was running.
- * The installer is now our native Java UI (Aria2Manager + TarExtractor).
- */
-
- // --- 6. Only the raw base app is present ---
- /*
- * Previously returned SystemState.TERMUX_ONLY.
- * Now it means the system is completely virgin (no rootfs, no variables).
- */
- return SystemState.NONE;
- }
-
- // --- METHODS FOR OBTAINING ARCHITECTURES ---
- private String getTermuxArch() {
- try {
- // Inspecting our own app's NDK instead of an external package
- android.content.pm.ApplicationInfo info = requireContext().getApplicationInfo();
- String nativeLibDir = info.nativeLibraryDir;
-
- if (nativeLibDir != null) {
- if (nativeLibDir.endsWith("arm64") || nativeLibDir.contains("arm64-v8a"))
- return "arm64-v8a";
- if (nativeLibDir.endsWith("arm") || nativeLibDir.contains("armeabi-v7a"))
- return "armeabi-v7a";
- if (nativeLibDir.endsWith("x86_64") || nativeLibDir.contains("x86_64"))
- return "x86_64";
- if (nativeLibDir.endsWith("x86") || nativeLibDir.contains("x86")) return "x86";
- }
- } catch (Exception e) {
- android.util.Log.e("IIAB-Dashboard", "Error obtaining native architecture", e);
- }
-
- if (android.os.Build.SUPPORTED_ABIS.length > 0) {
- return android.os.Build.SUPPORTED_ABIS[0];
- }
- return "unknown";
- }
-
- private String getDebianArch(String androidArch) {
- return SystemStatsUtil.getDebianArch(androidArch);
- }
-
- // Converter from DP to actual screen pixels
- private int dpToPx(int dp) {
- return (int) (dp * getResources().getDisplayMetrics().density);
- }
-
- @Override
- public void onConfigurationChanged(@NonNull android.content.res.Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- // Dynamically adjust the width when the user rotates the screen
- adaptLayoutToOrientation(newConfig.orientation);
- }
-
- /**
- * Applies responsive web design principles to the gauges container.
- * Prevents extreme stretching in landscape mode by limiting width to 75%.
- */
- private void adaptLayoutToOrientation(int orientation) {
- if (gaugesContainer == null) return;
-
- LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) gaugesContainer.getLayoutParams();
-
- if (orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
- // Landscape: Limit width to 75% of the screen for better UX
- int screenWidth = getResources().getDisplayMetrics().widthPixels;
- params.width = (int) (screenWidth * 0.75f);
- } else {
- // Portrait: Use full available width
- params.width = ViewGroup.LayoutParams.MATCH_PARENT;
- }
-
- gaugesContainer.setLayoutParams(params);
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/DashboardManager.java b/controller/app/src/main/java/org/iiab/controller/DashboardManager.java
deleted file mode 100644
index 3e4fb6542..000000000
--- a/controller/app/src/main/java/org/iiab/controller/DashboardManager.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * ============================================================================
- * Name : DashboardManager.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Home dashboard status helper: binds the Wi-Fi and Hotspot tiles
- * and reflects their OS connectivity state on the LEDs. The legacy
- * "tunnel"/ESPW toggle was removed with the dead SOCKS-proxy
- * mechanism (ADFA-4553); content is served from the native local
- * server, so there is no tunnel state to display.
- * ============================================================================
- */
-package org.iiab.controller;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.provider.Settings;
-import android.view.View;
-
-import org.iiab.controller.hotspot.LocalHotspotManager;
-
-public class DashboardManager {
-
- private final Activity activity;
-
- private final View dashWifi, dashHotspot;
- private final View ledWifi, ledHotspot;
-
- public DashboardManager(Activity activity, View rootView) {
- this.activity = activity;
-
- dashWifi = rootView.findViewById(R.id.dash_wifi);
- dashHotspot = rootView.findViewById(R.id.dash_hotspot);
- ledWifi = rootView.findViewById(R.id.led_wifi);
- ledHotspot = rootView.findViewById(R.id.led_hotspot);
-
- setupListeners();
- }
-
- private void setupListeners() {
- // Single tap opens Settings directly
- dashWifi.setOnClickListener(v -> activity.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)));
-
- dashHotspot.setOnClickListener(v -> {
- // ADFA-4520: record that the operator tried the native hotspot, so the
- // Usage tab can later recommend the LocalOnlyHotspot fallback ONLY when
- // this AND "no SIM" AND "hotspot still not up" all hold.
- LocalHotspotManager.get().markNativeHotspotAttempted();
- try {
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.setClassName("com.android.settings", "com.android.settings.TetherSettings");
- activity.startActivity(intent);
- } catch (Exception e) {
- activity.startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS));
- }
- });
- }
-
- // Updates the LED graphics based on actual OS connectivity states
- public void updateConnectivityLeds(boolean isWifiOn, boolean isHotspotOn) {
- ledWifi.setBackgroundResource(isWifiOn ? R.drawable.led_on_green : R.drawable.led_off);
- ledHotspot.setBackgroundResource(isHotspotOn ? R.drawable.led_on_green : R.drawable.led_off);
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java
deleted file mode 100644
index a2b8f6b73..000000000
--- a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java
+++ /dev/null
@@ -1,1071 +0,0 @@
-/*
- * ============================================================================
- * Name : DeployFragment.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Installation / deployment view (Refactored with SAF & Regions)
- * ============================================================================
- */
-package org.iiab.controller;
-
-import org.iiab.controller.config.BoxEndpoints;
-
-import org.iiab.controller.deploy.domain.ModuleName;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.util.Log;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.Looper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.RemoteInput;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.github.mikephil.charting.components.XAxis;
-import com.github.mikephil.charting.components.YAxis;
-import com.github.mikephil.charting.data.Entry;
-import com.github.mikephil.charting.data.LineData;
-import com.github.mikephil.charting.data.LineDataSet;
-import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.util.LocalVarsYamlParser;
-import org.iiab.controller.rootfs.domain.RootfsAbi;
-import org.iiab.controller.rootfs.domain.RootfsTier;
-import org.iiab.controller.rootfs.presentation.RootfsUiState;
-import org.iiab.controller.rootfs.presentation.RootfsViewModel;
-import org.iiab.controller.rootfs.presentation.RootfsViewModelFactory;
-import org.iiab.controller.util.ByteFormatter;
-import org.iiab.controller.util.ProcessRunner;
-
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-public class DeployFragment extends Fragment implements org.iiab.controller.backup.presentation.BackupHost,
- org.iiab.controller.install.presentation.PlannerHost,
- org.iiab.controller.install.presentation.InstallHost,
- org.iiab.controller.install.presentation.ResetDeleteHost,
- org.iiab.controller.install.presentation.AdbShareHost {
-
- private final org.iiab.controller.install.presentation.AdbShareController adbShareController =
- new org.iiab.controller.install.presentation.AdbShareController(this, this);
-
- private final org.iiab.controller.install.presentation.ResetDeleteController resetDeleteController =
- new org.iiab.controller.install.presentation.ResetDeleteController(this, this);
-
- private final org.iiab.controller.install.presentation.InstallController installController =
- new org.iiab.controller.install.presentation.InstallController(this, this);
-
- private final org.iiab.controller.install.presentation.PlannerController plannerController =
- new org.iiab.controller.install.presentation.PlannerController(this, this);
-
- private final org.iiab.controller.backup.presentation.BackupController backupController =
- new org.iiab.controller.backup.presentation.BackupController(this, this);
-
- // =========================================================================================
- // REGION 1: VARIABLES & STATE
- // =========================================================================================
- private static final String TAG = "IIAB-DeployFragment";
-
- // UI Variables
- private View ledInternet, ledDevMode, ledDcpr, ledPpk;
- private TextView txtDcpr, txtPpk, btnRefreshModules;
- private LinearLayout rolesContainer, discrepancyWarning;
- private Button btnAdvancedReset;
- private ProgressButton btnFastInstall, btnFastDelete, btnLaunchInstall;
- private Button btnAdvancedForceStop;
- private ProgressButton btnAdvancedBackup, btnAdvancedRestore;
- private LinearLayout restoreLogPanel;
- private TextView restoreLogText, restoreLogResult;
- private androidx.core.widget.NestedScrollView restoreLogScroll;
-
- // Backup Menu UI
- private TextView txtSelectBackupTitle, txtBackupStatus;
- private LinearLayout containerBackupList;
-
- // Advanced Monitoring UI
- private TextView txtAdvMonitoringTitle;
- private LinearLayout containerAdvMonitoring;
- private Button btnAdbAction;
- private View ledAdbStatus;
- private TextView txtAdbLedLabel;
- private com.github.mikephil.charting.charts.LineChart cpuChart;
-
- // Planner UI
- private Button btnTierBasic, btnTierStandard, btnTierFull;
- private TextView txtLegendIiab, txtLegendMaps, txtLegendKiwix, txtLegendFree;
- private TextView txtOfflineEstimate;
- private CheckBox chkCompanionData;
- private MultiResourceGaugeView storageGauge;
- private android.widget.Button btnKiwixSettings;
-
- // SAF & Backup Controls
- private Button btnImportBackup;
- private boolean isBackupInProgress = false;
-
- // State Variables
- private final List newInstallCheckboxes = new ArrayList<>();
- private File sharedStateDir;
- private JSONObject lastKnownState = new JSONObject();
- private List installationQueue = new ArrayList<>();
- private boolean isBatchInstalling = false;
- private boolean isStorageSafe = false;
- // Presentation-layer source of the OS rootfs size (live, with offline fallback).
- // Last known connectivity, refreshed by checkInternetAccess() (every 3s via liveStatusRunnable).
- private volatile boolean hasInternet = true;
-
- // Native Engine Variables
- // Download state (Aria2Manager + in-flight flag) is owned by an Activity-scoped
- // ViewModel so it survives Fragment recreation (e.g. rotation during a download)
- // without static mutable state. (ADFA-4459, D9)
- private org.iiab.controller.install.presentation.DownloadStateViewModel downloadState;
-
- // ADFA-4474 PR2: install progress is owned by InstallService and observed here.
- private boolean installProgressShown = false; // guards ProgressButton.startProgress()
- private long lastInstallTerminalSeq = -1L; // fires terminal snackbars exactly once
- private long lastResetTerminalSeq = -1L; // reset terminal snackbars, fired exactly once (ADFA-4476)
- private long lastModuleQueueTerminalSeq = -1L; // module-queue finish/fail snackbar, once (ADFA-4476 s3)
- // State Variables (New control variables)
- private boolean isRestoring = false;
- private boolean isDeleting = false;
- private boolean isImporting = false;
- private PRootEngine prootEngine;
-
- // Background Handlers
- private final Handler liveStatusHandler = new Handler(Looper.getMainLooper());
- private Runnable liveStatusRunnable;
-
- // ADB Variables
-
-
-
-
- // =========================================================================================
- // REGION 2: ANDROID LIFECYCLE
- // =========================================================================================
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_deploy, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- org.iiab.controller.help.TooltipWiring.wireAll(view);
-
- downloadState = new ViewModelProvider(requireActivity())
- .get(org.iiab.controller.install.presentation.DownloadStateViewModel.class);
- downloadState.installState().observe(getViewLifecycleOwner(), this::renderInstallProgress);
- // ADFA-4476 slice 3: the per-module queue is owned by InstallService; observe it so the
- // launch button + grid re-bind after a recreation and the finish/fail snackbar fires once.
- org.iiab.controller.install.presentation.ModuleQueueRepository.get().state()
- .observe(getViewLifecycleOwner(), this::renderModuleQueue);
-
- // UI Binding
- ledInternet = view.findViewById(R.id.led_install_internet);
- ledDevMode = view.findViewById(R.id.led_install_dev_mode);
- ledDcpr = view.findViewById(R.id.led_install_dcpr);
- ledPpk = view.findViewById(R.id.led_install_ppk);
- txtDcpr = view.findViewById(R.id.txt_install_dcpr);
- txtPpk = view.findViewById(R.id.txt_install_ppk);
- btnAdbAction = view.findViewById(R.id.btn_adb_action);
- ledAdbStatus = view.findViewById(R.id.led_adb_status);
- txtAdbLedLabel = view.findViewById(R.id.txt_adb_led_label);
- cpuChart = view.findViewById(R.id.cpu_chart);
- btnKiwixSettings = view.findViewById(R.id.btn_kiwix_settings);
- rolesContainer = view.findViewById(R.id.install_roles_container);
- discrepancyWarning = view.findViewById(R.id.install_discrepancy_warning);
- btnLaunchInstall = view.findViewById(R.id.btn_launch_install);
- btnFastInstall = view.findViewById(R.id.btn_fast_install);
- btnFastDelete = view.findViewById(R.id.btn_fast_delete);
- btnAdvancedReset = view.findViewById(R.id.btn_advanced_reset);
- btnAdvancedBackup = view.findViewById(R.id.btn_advanced_backup);
- btnAdvancedRestore = view.findViewById(R.id.btn_advanced_restore);
- btnAdvancedForceStop = view.findViewById(R.id.btn_advanced_force_stop);
- restoreLogPanel = view.findViewById(R.id.restore_log_panel);
- restoreLogText = view.findViewById(R.id.restore_log_text);
- restoreLogResult = view.findViewById(R.id.restore_log_result);
- restoreLogScroll = view.findViewById(R.id.restore_log_scroll);
- View restoreLogClose = view.findViewById(R.id.restore_log_close);
- if (restoreLogClose != null) {
- restoreLogClose.setOnClickListener(vv -> { if (restoreLogPanel != null) restoreLogPanel.setVisibility(View.GONE); });
- }
- txtSelectBackupTitle = view.findViewById(R.id.txt_select_backup_title);
- containerBackupList = view.findViewById(R.id.container_backup_list);
- txtBackupStatus = view.findViewById(R.id.txt_backup_status);
- btnRefreshModules = view.findViewById(R.id.btn_refresh_modules);
- btnTierBasic = view.findViewById(R.id.btn_tier_basic);
- btnTierStandard = view.findViewById(R.id.btn_tier_standard);
- btnTierFull = view.findViewById(R.id.btn_tier_full);
- chkCompanionData = view.findViewById(R.id.chk_companion_data);
- storageGauge = view.findViewById(R.id.storage_projection_gauge);
- txtLegendIiab = view.findViewById(R.id.txt_legend_iiab);
- txtLegendMaps = view.findViewById(R.id.txt_legend_maps);
- txtLegendKiwix = view.findViewById(R.id.txt_legend_kiwix);
- txtLegendFree = view.findViewById(R.id.txt_legend_free);
- txtOfflineEstimate = view.findViewById(R.id.txt_offline_estimate);
-
- // SAF Binding
- btnImportBackup = view.findViewById(R.id.btn_import_backup);
-
- sharedStateDir = new File(Environment.getExternalStorageDirectory(), ".iiab_state");
-
- // Initialization Logic
- backupController.registerLaunchers();
- adbShareController.onViewCreated(ledAdbStatus, ledDcpr, ledPpk, txtDcpr, txtPpk, txtAdbLedLabel, btnAdbAction);
- setupAdvancedMonitoringMenu(view);
- setupCpuChart();
- plannerController.bind(rolesContainer, storageGauge, btnTierBasic, btnTierStandard, btnTierFull,
- txtLegendIiab, txtLegendMaps, txtLegendKiwix, txtLegendFree, txtOfflineEstimate,
- btnKiwixSettings, chkCompanionData);
- setupAllCollapsibleMenus();
- plannerController.createModulesGrid();
-
- // Initial States
- btnLaunchInstall.setEnabled(false);
- btnLaunchInstall.setAlpha(0.5f);
- int kiwixTint = ContextCompat.getColor(requireContext(), R.color.text_secondary);
- btnKiwixSettings.setCompoundDrawableTintList(android.content.res.ColorStateList.valueOf(kiwixTint));
- btnKiwixSettings.setTextColor(kiwixTint);
- btnFastInstall.setAlpha(0.4f);
-
- // Handlers
- liveStatusRunnable = () -> {
- new Thread(() -> {
- boolean isAlive = pingUrl(BoxEndpoints.BASE + "/home");
- checkInternetAccess();
- if (isAdded() && getActivity() != null) {
- getActivity().runOnUiThread(this::updateDynamicButtons);
- }
- }).start();
- liveStatusHandler.postDelayed(liveStatusRunnable, 3000);
- };
-
- requestFreshLocalVarsSilently();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (discrepancyWarning != null) discrepancyWarning.setVisibility(View.GONE);
-
- adbShareController.onResume();
- checkAndHandleSyncFragmentFocus();
- restoreQueueFromPrefs();
- // ADFA-4476 slice 3: no longer re-fire the queue here. The foreground InstallService
- // owns the module loop and keeps advancing across a recreation; we only observe it
- // (see renderModuleQueue) and re-render the grid below.
-
- if (lastKnownState.length() > 0) {
- installController.verifyInstallationState(lastKnownState);
- } else {
- loadLocalVarsFallback();
- }
-
- updateDynamicButtons();
- liveStatusHandler.post(liveStatusRunnable);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- adbShareController.onPause();
- liveStatusHandler.removeCallbacks(liveStatusRunnable);
- }
-
- /** Renders the observable install progress published by InstallService.
- * Re-binds automatically after a recreation or backgrounding (ADFA-4474 PR2). */
- private void renderInstallProgress(org.iiab.controller.install.presentation.InstallState s) {
- if (s == null) return;
- // Scratch reset shares the same repository/observer (ADFA-4476); render it apart.
- if (s.op == org.iiab.controller.install.presentation.InstallState.Op.RESET) {
- renderResetProgress(s);
- return;
- }
- if (btnFastInstall == null) return;
- org.iiab.controller.install.presentation.InstallState.Phase p = s.phase;
-
- if (s.isRunning() && !installProgressShown) {
- installProgressShown = true;
- btnFastInstall.setAlpha(0.8f);
- btnFastInstall.setTextSize(12f);
- btnFastInstall.startProgress();
- }
-
- switch (p) {
- case DOWNLOADING:
- btnFastInstall.setText(getString(R.string.install_status_os_download, s.percent, s.speed));
- break;
- case VERIFYING: {
- // ADFA-5118: legacy button shows the verify % (or "…" before the first byte).
- String vlbl = org.iiab.controller.deploy.domain.ExtractProgress.firstLine(
- getString(R.string.k2go_verifying_files));
- btnFastInstall.setText(s.percent < 0
- ? (getString(R.string.k2go_reading) + " …")
- : (vlbl + " " + s.percent + "%"));
- break;
- }
- case EXTRACTING: {
- // ADFA-4915: legacy button shows the extract % (or "…" during the reading sub-phase).
- String lbl = org.iiab.controller.deploy.domain.ExtractProgress.firstLine(
- getString(R.string.install_status_extracting));
- btnFastInstall.setText(s.percent < 0
- ? (getString(R.string.k2go_reading) + " …")
- : (lbl + " " + s.percent + "%"));
- break;
- }
- case PROVISIONING:
- // ADFA-5119: SOFTFAILED counts as running, so the button is already in its progress
- // state; the message says what stopped it. This screen has no Retry — the control lives
- // on the boot gate — and grafting one on is not worth it for a screen no shipping flow
- // reaches (see the reachability note in InstallController).
- case SOFTFAILED:
- btnFastInstall.setText(s.message);
- break;
- case SUCCESS:
- installProgressShown = false;
- btnFastInstall.stopProgress();
- btnFastInstall.setText(R.string.install_btn_reinstall);
- btnFastInstall.setAlpha(1.0f);
- updateDynamicButtons();
- if (s.seq > lastInstallTerminalSeq) {
- lastInstallTerminalSeq = s.seq;
- requestFreshLocalVarsSilently();
- if (getActivity() instanceof MainActivity) // ADFA-4519
- ((MainActivity) getActivity()).showSnackbar(getString(R.string.install_success_deployment));
- }
- break;
- // ADFA-5119: a cancelled rootfs build now reports CANCELLED rather than FAILED, and this
- // screen has to answer it or the button stays stuck showing the last percentage it saw.
- // Same body as FAILED on purpose: the button goes back to "Install" either way. The
- // snackbar below is skipped by itself — a cancellation carries no message, because the
- // user is the one who asked for it and does not need to be told it happened.
- case CANCELLED:
- case FAILED:
- installProgressShown = false;
- btnFastInstall.stopProgress();
- btnFastInstall.setText(R.string.install_btn_install);
- btnFastInstall.setAlpha(1.0f);
- updateDynamicButtons();
- if (s.seq > lastInstallTerminalSeq) {
- lastInstallTerminalSeq = s.seq;
- if (getActivity() instanceof MainActivity && !s.message.isEmpty()) // ADFA-4519
- ((MainActivity) getActivity()).showSnackbar(s.message);
- }
- break;
- case IDLE:
- default:
- installProgressShown = false;
- break;
- }
- }
-
- /** Renders the scratch-reset progress (ADFA-4476), tagged Op.RESET on the shared
- * repository so it survives a recreation just like the install flow. */
- private void renderResetProgress(org.iiab.controller.install.presentation.InstallState s) {
- if (btnAdvancedReset == null) return;
- switch (s.phase) {
- case DOWNLOADING:
- btnAdvancedReset.setEnabled(true);
- if (s.percent <= 0) {
- btnAdvancedReset.setText(getString(R.string.install_status_downloading_debian));
- } else {
- btnAdvancedReset.setText(getString(R.string.install_status_debian_download, s.percent, s.speed)
- + "\n(Tap to Cancel)");
- }
- break;
- case VERIFYING: // ADFA-5118: reset (if it reinstalls the rootfs) shares the busy label
- case EXTRACTING:
- case PROVISIONING:
- // Wiping / extracting / bootstrapping: message is supplied by the service.
- btnAdvancedReset.setText(s.message);
- btnAdvancedReset.setEnabled(false);
- break;
- case SUCCESS:
- btnAdvancedReset.setText(R.string.install_btn_reset);
- btnAdvancedReset.setEnabled(true);
- updateDynamicButtons();
- if (s.seq > lastResetTerminalSeq) {
- lastResetTerminalSeq = s.seq;
- if (getView() != null)
- Snackbars.make(getView(), R.string.install_success_vanilla).show();
- }
- break;
- case FAILED:
- btnAdvancedReset.setText(R.string.install_btn_reset);
- btnAdvancedReset.setEnabled(true);
- updateDynamicButtons();
- if (s.seq > lastResetTerminalSeq) {
- lastResetTerminalSeq = s.seq;
- if (getView() != null && !s.message.isEmpty())
- Snackbars.make(getView(), s.message).show();
- }
- break;
- case IDLE:
- default:
- btnAdvancedReset.setText(R.string.install_btn_reset);
- btnAdvancedReset.setEnabled(true);
- break;
- }
- }
-
- /** Renders the per-module install queue owned by InstallService (ADFA-4476 slice 3).
- * Re-binds after a recreation, so a theme toggle mid-queue keeps advancing and the
- * finish/fail snackbar fires exactly once. */
- private void renderModuleQueue(org.iiab.controller.install.presentation.ModuleQueueState s) {
- if (s == null) return;
- switch (s.phase) {
- case RUNNING:
- updateDynamicButtons();
- if (btnLaunchInstall != null) {
- if (s.currentModule != null)
- btnLaunchInstall.setText(getString(R.string.install_status_installing_module, s.currentModule));
- // ADFA-4476: same indeterminate progress bar as the other action buttons.
- // startProgress() is idempotent (guards on isRunning) so re-observing after a
- // recreation does not restart it.
- btnLaunchInstall.startProgress();
- }
- break;
- case DONE:
- if (s.seq > lastModuleQueueTerminalSeq) {
- lastModuleQueueTerminalSeq = s.seq;
- // Installed modules drop out of the selection; failed ones stay checked so
- // the user can retry them. selectedModuleKeys survives recreation (slice 1).
- for (String key : new java.util.HashSet<>(selectedModuleKeys())) {
- if (!s.failedModules.contains(key)) selectedModuleKeys().remove(key);
- }
- if (getActivity() instanceof MainActivity) {
- MainActivity act = (MainActivity) getActivity();
- if (s.failedModules.isEmpty()) {
- act.showSnackbar(getString(R.string.install_msg_finished));
- } else {
- act.showSnackbar(getString(R.string.install_msg_failed,
- android.text.TextUtils.join(", ", s.failedModules)));
- }
- }
- if (btnLaunchInstall != null) {
- btnLaunchInstall.stopProgress(); // ADFA-4476: stop the progress bar
- btnLaunchInstall.setEnabled(false);
- btnLaunchInstall.setText(getString(R.string.install_btn_launch));
- }
- updateDynamicButtons();
- // Refresh the grid from the (now settled) local_vars.yml + ping.
- installController.fetchLocalVarsFromPRoot();
- }
- break;
- case IDLE:
- default:
- break;
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- // ADFA-4474 PR2: the install now runs in InstallService and must survive
- // leaving this screen (and configuration changes). Cancellation is explicit
- // via the button / notification, so we no longer stop it here.
- }
-
-
- // =========================================================================================
- // REGION 3: UI & MENU CONTROLLERS
- // =========================================================================================
-
- private void setupAllCollapsibleMenus() {
- if (getView() == null) return;
- TextView txtModuleMgmtTitle = getView().findViewById(R.id.txt_module_mgmt_title);
- LinearLayout containerModuleMgmt = getView().findViewById(R.id.container_module_mgmt);
- setupSingleMenu(txtModuleMgmtTitle, containerModuleMgmt, R.string.install_header_roles);
-
- TextView txtMaintenanceTitle = getView().findViewById(R.id.txt_maintenance_title);
- LinearLayout containerMaintenance = getView().findViewById(R.id.container_maintenance);
- setupSingleMenu(txtMaintenanceTitle, containerMaintenance, R.string.install_header_maintenance);
- }
-
- private void setupSingleMenu(TextView titleView, View container, int stringRes) {
- if (titleView == null || container == null) return;
- container.setVisibility(View.GONE);
- String baseText = getString(stringRes);
- titleView.setText(getString(R.string.label_separator_up, baseText));
- titleView.setOnClickListener(v -> {
- boolean isCollapsed = container.getVisibility() == View.GONE;
- container.setVisibility(isCollapsed ? View.VISIBLE : View.GONE);
- titleView.setText(getString(isCollapsed ? R.string.label_separator_down : R.string.label_separator_up, baseText));
- });
- }
-
- private void setupAdvancedMonitoringMenu(View view) {
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
- view.findViewById(R.id.section_adv_monitoring).setVisibility(View.GONE);
- View adbLedsContainer = view.findViewById(R.id.container_adb_leds);
- if (adbLedsContainer != null) adbLedsContainer.setVisibility(View.GONE);
- } else {
- txtAdvMonitoringTitle = view.findViewById(R.id.txt_adv_monitoring_title);
- containerAdvMonitoring = view.findViewById(R.id.container_adv_monitoring);
- setupSingleMenu(txtAdvMonitoringTitle, containerAdvMonitoring, R.string.install_adv_monitoring_title);
- }
- }
-
- private void focusAdvancedMonitoring() {
- if (containerAdvMonitoring != null && txtAdvMonitoringTitle != null) {
- if (containerAdvMonitoring.getVisibility() == View.GONE)
- txtAdvMonitoringTitle.performClick();
- android.animation.ArgbEvaluator evaluator = new android.animation.ArgbEvaluator();
- android.animation.ObjectAnimator animator = android.animation.ObjectAnimator.ofObject(
- txtAdvMonitoringTitle, "textColor", evaluator,
- ContextCompat.getColor(requireContext(), R.color.status_danger), ContextCompat.getColor(requireContext(), R.color.dash_text_primary)
- );
- animator.setDuration(400);
- animator.setRepeatCount(5);
- animator.setRepeatMode(android.animation.ValueAnimator.REVERSE);
- animator.start();
- }
- }
-
- private void checkAndHandleSyncFragmentFocus() {
- android.content.SharedPreferences adbPrefs = requireContext().getSharedPreferences("iiab_adb_prefs", Context.MODE_PRIVATE);
- if (adbPrefs.getBoolean("pairing_just_succeeded", false)) {
- adbPrefs.edit().putBoolean("pairing_just_succeeded", false).apply();
- btnAdbAction.setText(R.string.securing_connection);
- new Handler(Looper.getMainLooper()).postDelayed(() -> adbShareController.startAdbPairingFlow(true), 2500);
- }
- if (adbPrefs.getBoolean("focus_adb", false)) {
- adbPrefs.edit().putBoolean("focus_adb", false).apply();
- new Handler(Looper.getMainLooper()).postDelayed(this::focusAdvancedMonitoring, 600);
- }
- }
-
-
- public void updateDynamicButtons() {
- MainActivity mainAct = (MainActivity) getActivity();
- if (mainAct == null || !isAdded()) return;
-
- boolean isServerRunning = ServerStateRepository.get().current().alive;
- final File iiabRootDir = new File(requireContext().getFilesDir(), "rootfs");
- final File debianRootfs = new File(iiabRootDir, "installed-rootfs/iiab");
- final File backupsDir = new File(iiabRootDir, "backups");
- if (!backupsDir.exists()) backupsDir.mkdirs();
-
- boolean isProotInstalled = new File(debianRootfs, "etc/os-release").exists() || new File(debianRootfs, "usr/bin/bash").exists();
- refreshDashboardLeds(mainAct);
-
- // Animated Banner
- View bannerWarning = getView().findViewById(R.id.banner_server_warning);
- if (bannerWarning != null) {
- boolean isBannerVisible = bannerWarning.getVisibility() == View.VISIBLE;
- if (isServerRunning && !isBannerVisible) {
- android.transition.TransitionManager.beginDelayedTransition((ViewGroup) getView(), new android.transition.AutoTransition().setDuration(300));
- bannerWarning.setVisibility(View.VISIBLE);
- } else if (!isServerRunning && isBannerVisible) {
- android.transition.TransitionManager.beginDelayedTransition((ViewGroup) getView(), new android.transition.AutoTransition().setDuration(300));
- bannerWarning.setVisibility(View.GONE);
- }
- }
-
- // Refresh Button
- if (btnRefreshModules != null) {
- btnRefreshModules.setEnabled(true);
- if (isServerRunning || !isProotInstalled) {
- btnRefreshModules.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_disabled));
- btnRefreshModules.setAlpha(0.6f);
- btnRefreshModules.setOnClickListener(v -> {
- if (!isProotInstalled)
- Snackbars.make(v, R.string.install_msg_termux_missing).show();
- else if (isServerRunning)
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- });
- } else {
- btnRefreshModules.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_info));
- btnRefreshModules.setAlpha(1.0f);
- btnRefreshModules.setOnClickListener(v -> {
- v.setAlpha(0.5f);
- requestFreshLocalVars();
- new Handler(Looper.getMainLooper()).postDelayed(() -> v.setAlpha(1.0f), 1000);
- });
- }
- }
-
- // Basic Enablers
- btnFastInstall.setEnabled(true);
- btnFastDelete.setEnabled(true);
- if (btnAdvancedReset != null) btnAdvancedReset.setEnabled(true);
- if (btnAdvancedBackup != null) btnAdvancedBackup.setEnabled(true);
- if (btnAdvancedRestore != null) btnAdvancedRestore.setEnabled(true);
- if (txtSelectBackupTitle != null) txtSelectBackupTitle.setEnabled(true);
-
- if (btnAdvancedForceStop != null) {
- btnAdvancedForceStop.setEnabled(true);
- btnAdvancedForceStop.setAlpha(1.0f);
- btnAdvancedForceStop.setOnClickListener(v -> openTermuxAppInfo());
- }
-
- boolean isBusy = isSystemBusy();
-
- // 1. ALWAYS link the buttons so Listeners can intercept and drop the Snackbar
- installController.bind(mainAct, debianRootfs, iiabRootDir,
- btnFastInstall, btnLaunchInstall, discrepancyWarning, rolesContainer, chkCompanionData);
- resetDeleteController.bind(mainAct, debianRootfs, btnAdvancedReset, btnFastDelete);
- backupController.bind(mainAct, backupsDir, iiabRootDir,
- btnImportBackup, btnAdvancedBackup, btnAdvancedRestore,
- txtSelectBackupTitle, txtBackupStatus, containerBackupList,
- restoreLogPanel, restoreLogText, restoreLogResult, restoreLogScroll);
-
- if (isServerRunning || isBusy) {
- // LOCK MODE: Server On or System Busy
- float lockAlpha = 0.5f;
-
- // We keep the opacity at 80% only for the button that is currently working.
- // Install and reset share isDownloadingRootfs() (ADFA-4476), so tell them
- // apart by the running operation: otherwise a reset would leave the install
- // button looking "active" instead of dimmed like the rest.
- boolean rootfsOp = isDownloadingRootfs() && !isServerRunning;
- org.iiab.controller.install.presentation.InstallState.Op runningOp =
- org.iiab.controller.install.presentation.InstallProgressRepository.get().currentOp();
- boolean installWorking = rootfsOp
- && runningOp == org.iiab.controller.install.presentation.InstallState.Op.INSTALL;
- boolean resetWorking = rootfsOp
- && runningOp == org.iiab.controller.install.presentation.InstallState.Op.RESET;
- btnFastInstall.setAlpha(installWorking ? 0.8f : lockAlpha);
- btnFastDelete.setAlpha(isDeleting ? 0.8f : lockAlpha);
- if (btnAdvancedBackup != null) btnAdvancedBackup.setAlpha(isBackupInProgress ? 0.8f : lockAlpha);
- if (btnAdvancedRestore != null) btnAdvancedRestore.setAlpha(isRestoring ? 0.8f : lockAlpha);
- if (btnAdvancedReset != null) btnAdvancedReset.setAlpha(resetWorking ? 0.8f : lockAlpha);
- if (txtSelectBackupTitle != null) txtSelectBackupTitle.setAlpha(lockAlpha);
- if (btnImportBackup != null) btnImportBackup.setAlpha(isImporting ? 0.8f : lockAlpha);
-
- // Lock module checkboxes in the grid
- for (CheckBox cb : newInstallCheckboxes) {
- cb.setEnabled(false);
- View card = (View) cb.getParent().getParent();
- card.setAlpha(0.6f);
- card.setOnClickListener(v -> {
- String msg = isServerRunning ? getString(R.string.install_msg_server_running_lock) : getSystemBusyMessage();
- Snackbars.make(v, msg).show();
- });
- }
-
- } else {
- // FREE MODE: All off and ready to operate
- if (!hasInternet || getSelectedTier() == null || !isStorageSafe) btnFastInstall.setAlpha(0.4f);
- else btnFastInstall.setAlpha(1.0f);
-
- btnFastDelete.setAlpha(1.0f);
- if (btnAdvancedBackup != null) btnAdvancedBackup.setAlpha(1.0f);
- if (btnAdvancedReset != null) btnAdvancedReset.setAlpha(1.0f);
- if (txtSelectBackupTitle != null) txtSelectBackupTitle.setAlpha(1.0f);
- if (btnImportBackup != null) btnImportBackup.setAlpha(1.0f);
-
- btnFastInstall.setEnabled(true);
- btnFastInstall.setTextSize(14f);
- if (!hasInternet) {
- // Offline: downloading is impossible. Signal it on the button itself;
- // the click listener shows a snackbar instead of starting a failing download.
- btnFastInstall.setText(R.string.install_btn_no_connection);
- } else {
- btnFastInstall.setText(isProotInstalled ? R.string.install_btn_reinstall : R.string.install_btn_install);
- }
-
- // Unlock checkboxes
- for (CheckBox cb : newInstallCheckboxes) {
- cb.setEnabled(true);
- View card = (View) cb.getParent().getParent();
- card.setAlpha(1.0f);
- card.setOnClickListener(v -> cb.toggle());
- }
- }
- }
-
-
- // =========================================================================================
- // REGION 4: INSTALLATION PLANNER
- // =========================================================================================
-
-
-
- /**
- * Completes the storage projection once {@link RootfsViewModel} resolves the OS
- * size (live, or the offline fallback). The UI now consumes the size from the
- * presentation layer instead of having {@link InstallationPlanner} resolve it.
- */
-
-
-
- // =========================================================================================
- // REGION 5: NATIVE PIPELINES
- // =========================================================================================
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- // =========================================================================================
- // REGION 6: BACKUP & RESTORE SAF
- // =========================================================================================
-
-
-
- // =========================================================================================
- // REGION 7: ADB & SYSTEM RESTRICTIONS
- // =========================================================================================
-
-
-
-
-
-
-
- private int getDynamicAdbPort(int fallbackPort) {
- try {
- Process process = Runtime.getRuntime().exec("getprop service.adb.tls.port");
- BufferedReader reader = new BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
- String portStr = reader.readLine();
- reader.close();
- if (portStr != null && !portStr.trim().isEmpty())
- return Integer.parseInt(portStr.trim());
- } catch (Exception ignored) {
- }
- return fallbackPort;
- }
-
-
-
-
-
-
-
-
-
- private void setupCpuChart() {
- cpuChart.getDescription().setEnabled(false);
- cpuChart.setTouchEnabled(false);
- cpuChart.setDrawGridBackground(false);
- cpuChart.getLegend().setEnabled(false);
-
- XAxis xAxis = cpuChart.getXAxis();
- xAxis.setDrawLabels(false);
- xAxis.setDrawGridLines(true);
- xAxis.setGridColor(ContextCompat.getColor(requireContext(), R.color.divider_line));
- xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
-
- YAxis leftAxis = cpuChart.getAxisLeft();
- leftAxis.setTextColor(ContextCompat.getColor(requireContext(), R.color.dash_text_secondary));
- leftAxis.setAxisMaximum(100f);
- leftAxis.setAxisMinimum(0f);
- leftAxis.setDrawGridLines(true);
- leftAxis.setGridColor(ContextCompat.getColor(requireContext(), R.color.divider_line));
-
- cpuChart.getAxisRight().setEnabled(false);
- cpuChart.setData(new LineData());
- }
-
- public void addCpuEntry(float cpuPercentage) {
- if (cpuChart == null || cpuChart.getData() == null) return;
- LineData data = cpuChart.getData();
- ILineDataSet set = data.getDataSetByIndex(0);
-
- if (set == null) {
- LineDataSet newSet = new LineDataSet(null, "CPU");
- newSet.setAxisDependency(YAxis.AxisDependency.LEFT);
- newSet.setColor(ContextCompat.getColor(requireContext(), R.color.accent));
- newSet.setLineWidth(2f);
- newSet.setDrawCircles(false);
- newSet.setDrawValues(false);
- newSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
- newSet.setDrawFilled(true);
- newSet.setFillColor(ContextCompat.getColor(requireContext(), R.color.accent));
- newSet.setFillAlpha(50);
- set = newSet;
- data.addDataSet(set);
- }
-
- data.addEntry(new Entry(set.getEntryCount(), cpuPercentage), 0);
- data.notifyDataChanged();
- cpuChart.notifyDataSetChanged();
- cpuChart.setVisibleXRangeMaximum(60);
- cpuChart.moveViewToX(data.getEntryCount());
- }
-
-
- // =========================================================================================
- // REGION 8: UTILITIES
- // =========================================================================================
-
- public boolean isSystemBusy() {
- return isDownloadingRootfs() || isBatchInstalling() || isBackupInProgress || isRestoring || isDeleting || isImporting;
- }
-
- public String getSystemBusyMessage() {
- if (isDownloadingRootfs()) return getString(R.string.install_busy_provisioning);
- if (isBatchInstalling()) return getString(R.string.install_busy_modules);
- if (isBackupInProgress) return getString(R.string.install_busy_backup);
- if (isRestoring) return getString(R.string.install_busy_restore);
- if (isDeleting) return getString(R.string.install_busy_delete);
- if (isImporting) return getString(R.string.install_busy_import);
- return getString(R.string.install_busy_generic);
- }
-
- public void enableSystemProtection() {
- // SAFE CHECK
- if (!isAdded() || getContext() == null) return;
-
- try {
- Intent intent = new Intent(requireContext(), WatchdogService.class);
- intent.setAction(WatchdogService.ACTION_START);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- requireContext().startForegroundService(intent);
- } else {
- requireContext().startService(intent);
- }
- } catch (Exception ignored) {
- }
- }
-
- public void disableSystemProtection() {
- // SAFE CHECK
- if (!isAdded() || getContext() == null) return;
-
- try {
- Intent intent = new Intent(requireContext(), WatchdogService.class);
-// intent.setAction(WatchdogService.ACTION_START);
- intent.setAction(WatchdogService.ACTION_STOP);
- requireContext().startService(intent);
- } catch (Exception ignored) {
- }
- }
-
-
- private void restoreQueueFromPrefs() {
- if (getActivity() == null) return;
- android.content.SharedPreferences prefs = getActivity().getSharedPreferences("iiab_queue_prefs", android.content.Context.MODE_PRIVATE);
- isBatchInstalling = prefs.getBoolean("is_batch_installing", false);
- String queueString = prefs.getString("pending_modules", "");
- installationQueue.clear();
- if (!queueString.isEmpty()) {
- String[] modules = queueString.split(",");
- installationQueue.addAll(java.util.Arrays.asList(modules));
- }
- }
-
- public boolean pingUrl(String urlStr) {
- try {
- URL url = new URL(urlStr);
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setUseCaches(false);
- conn.setInstanceFollowRedirects(false);
- conn.setConnectTimeout(1500);
- conn.setReadTimeout(1500);
- conn.setRequestMethod("HEAD");
- int responseCode = conn.getResponseCode();
- return (responseCode >= 200 && responseCode < 400);
- } catch (Exception e) {
- return false;
- }
- }
-
-
- public String getTermuxArch() {
- try {
- android.content.pm.ApplicationInfo info = requireContext().getApplicationInfo();
- String nativeLibDir = info.nativeLibraryDir;
- if (nativeLibDir != null) {
- if (nativeLibDir.endsWith("arm64") || nativeLibDir.contains("arm64-v8a"))
- return "arm64-v8a";
- if (nativeLibDir.endsWith("arm") || nativeLibDir.contains("armeabi-v7a"))
- return "armeabi-v7a";
- if (nativeLibDir.endsWith("x86_64") || nativeLibDir.contains("x86_64"))
- return "x86_64";
- if (nativeLibDir.endsWith("x86") || nativeLibDir.contains("x86")) return "x86";
- }
- } catch (Exception ignored) {
- }
- if (android.os.Build.SUPPORTED_ABIS.length > 0) return android.os.Build.SUPPORTED_ABIS[0];
- return "unknown";
- }
-
- /** Maps the legacy planner tier to the domain {@link RootfsTier}. */
-
- /** Detects the device ABI for rootfs selection, reusing {@link #getTermuxArch()}. */
-
- public void openTermuxAppInfo() {
- try {
- android.content.Intent intent = new android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
- android.net.Uri uri = android.net.Uri.fromParts("package", requireContext().getPackageName(), null);
- intent.setData(uri);
- startActivity(intent);
- } catch (Exception ignored) {
- }
- }
-
- private void checkInternetAccess() {
- new Thread(() -> {
- boolean hasInternet = false;
- try {
- URL url = new URL("https://clients3.google.com/generate_204");
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setUseCaches(false);
- conn.setConnectTimeout(2000);
- conn.setReadTimeout(2000);
- conn.setRequestMethod("HEAD");
- hasInternet = (conn.getResponseCode() == 204 || conn.getResponseCode() == 200);
- } catch (Exception e) {
- hasInternet = false;
- }
- final boolean isOnline = hasInternet;
- DeployFragment.this.hasInternet = isOnline;
- if (isAdded() && getActivity() != null) {
- getActivity().runOnUiThread(() -> {
- if (ledInternet != null) {
- if (isOnline) {
- ledInternet.setBackgroundResource(R.drawable.led_on_green);
- ledInternet.setBackgroundTintList(null);
- } else {
- ledInternet.setBackgroundResource(R.drawable.led_off);
- ledInternet.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.status_danger)));
- }
- }
- });
- }
- }).start();
- }
-
- private void loadLocalVarsFallback() {
- File jsonFile = new File(sharedStateDir, "local_vars.json");
- if (jsonFile.exists() && jsonFile.length() > 0) {
- try {
- StringBuilder text = new StringBuilder();
- BufferedReader br = new BufferedReader(new FileReader(jsonFile));
- String line;
- while ((line = br.readLine()) != null) text.append(line);
- br.close();
- lastKnownState = new JSONObject(text.toString());
- installController.verifyInstallationState(lastKnownState);
- } catch (Exception ignored) {
- }
- }
- }
-
- private void refreshDashboardLeds(MainActivity mainAct) {
- if (mainAct == null) return;
- boolean isDevModeOn = false;
- try {
- isDevModeOn = android.provider.Settings.Global.getInt(
- requireContext().getContentResolver(),
- android.provider.Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
- } catch (Exception ignored) {
- }
- if (ledDevMode != null)
- ledDevMode.setBackgroundResource(isDevModeOn ? R.drawable.led_on_green : R.drawable.led_off);
- }
-
- public float parseCpuUsage(String cpuLine) {
- try {
- java.util.regex.Pattern p = java.util.regex.Pattern.compile("(\\d+)%cpu.*?(\\d+)%idle");
- java.util.regex.Matcher m = p.matcher(cpuLine.toLowerCase());
- if (m.find()) {
- float totalCpu = Float.parseFloat(m.group(1));
- float idleCpu = Float.parseFloat(m.group(2));
- if (totalCpu > 0) return ((totalCpu - idleCpu) / totalCpu) * 100f;
- }
- } catch (Exception ignored) {
- }
- return -1f;
- }
-
- private void requestFreshLocalVars() {
- installController.fetchLocalVarsFromPRoot();
- }
-
- public void requestFreshLocalVarsSilently() {
- installController.fetchLocalVarsFromPRoot();
- }
-
- // --- BackupHost seam (backup/restore logic lives in BackupController) ---
- @Override public void setImporting(boolean importing) { this.isImporting = importing; }
- @Override public void setRestoring(boolean restoring) { this.isRestoring = restoring; }
- @Override public void setBackupInProgress(boolean inProgress) { this.isBackupInProgress = inProgress; }
- @Override public boolean isBackupInProgress() { return this.isBackupInProgress; }
-
- // --- PlannerHost seam (planner logic lives in PlannerController) ---
- @Override public InstallationPlanner.Tier getSelectedTier() { return downloadState.getSelectedTier(); }
- @Override public java.util.Set selectedModuleKeys() { return downloadState.getSelectedModuleKeys(); }
- @Override public void setSelectedTier(InstallationPlanner.Tier tier) { downloadState.setSelectedTier(tier); }
- @Override public boolean isCompanionData() { return downloadState.isCompanionData(); }
- @Override public void setCompanionData(boolean v) { downloadState.setCompanionData(v); }
- @Override public java.util.List moduleCheckboxes() { return newInstallCheckboxes; }
- @Override public void setStorageSafe(boolean safe) { this.isStorageSafe = safe; }
- @Override public boolean isStorageSafe() { return this.isStorageSafe; }
- @Override public String getOverrideKiwixLang() { return downloadState.getOverrideKiwixLang(); }
- @Override public void setOverrideKiwixLang(String lang) { downloadState.setOverrideKiwixLang(lang); }
- @Override public String getOverrideKiwixVariant() { return downloadState.getOverrideKiwixVariant(); }
- @Override public void setOverrideKiwixVariant(String variant) { downloadState.setOverrideKiwixVariant(variant); }
- @Override public boolean hasInternet() { return this.hasInternet; }
-
- // --- InstallHost seam (install pipeline lives in InstallController) ---
- // ADFA-4474 PR2: InstallProgressRepository is the single source of truth for
- // "an install is in flight" (survives recreation; the InstallService is the writer).
- @Override public boolean isDownloadingRootfs() { return org.iiab.controller.install.presentation.InstallProgressRepository.get().isRunning(); }
- @Override public void setDownloadingRootfs(boolean v) { /* no-op: derived from the repository now */ }
- // ADFA-4476 slice 3: batch-installing is now derived from the service-owned repository
- // (single source of truth), so it also covers a queue started before a recreation.
- @Override public boolean isBatchInstalling() { return org.iiab.controller.install.presentation.ModuleQueueRepository.get().isRunning(); }
- @Override public void setBatchInstalling(boolean v) { /* no-op: derived from ModuleQueueRepository now */ }
- @Override public java.util.List installationQueue() { return installationQueue; }
- @Override public org.json.JSONObject getLastKnownState() { return lastKnownState; }
- @Override public void setLastKnownState(org.json.JSONObject v) { lastKnownState = v; }
- @Override public org.iiab.controller.Aria2Manager aria2Manager() { return downloadState.getAria2Manager(); }
- @Override public void setAria2Manager(org.iiab.controller.Aria2Manager v) { downloadState.setAria2Manager(v); }
- @Override public org.iiab.controller.PRootEngine prootEngine() { return prootEngine; }
- @Override public void setPRootEngine(org.iiab.controller.PRootEngine v) { prootEngine = v; }
-
- // --- ResetDeleteHost seam ---
- @Override public boolean isDeleting() { return isDeleting; }
- @Override public void setDeleting(boolean v) { isDeleting = v; }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/controller/app/src/main/java/org/iiab/controller/MainActivity.java
deleted file mode 100644
index a521e007b..000000000
--- a/controller/app/src/main/java/org/iiab/controller/MainActivity.java
+++ /dev/null
@@ -1,963 +0,0 @@
-/*
- ============================================================================
- Name : MainActivity.java
- Contributors: IIAB Project
- Copyright (c) 2026 IIAB Project
- Description : Main Activity
- ============================================================================
- */
-
-package org.iiab.controller;
-
-import org.iiab.controller.util.AppExecutors;
-
-import android.Manifest;
-import android.os.Bundle;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.app.AppCompatDelegate;
-
-import android.content.Intent;
-import android.content.Context;
-import android.content.IntentFilter;
-import android.content.BroadcastReceiver;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Environment;
-import android.util.Log;
-import org.iiab.controller.update.presentation.UpdateController;
-import androidx.lifecycle.ViewModelProvider;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.os.Build;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.net.wifi.WifiManager;
-
-import androidx.core.content.ContextCompat;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import androidx.viewpager2.widget.ViewPager2;
-
-import android.view.MotionEvent;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-public class MainActivity extends AppCompatActivity implements TerminalController.Host, ServerController.Host, View.OnClickListener {
- private static final String TAG = "IIAB-MainActivity";
- public Preferences prefs;
- private ImageButton themeToggle;
- private ImageButton btnSettings;
- private android.widget.ImageView headerIcon;
-
- private UpdateController updateController;
-
- // Tabs UI
- private TabLayout tabLayout;
- private ViewPager2 viewPager;
- private TextView versionFooter;
- public boolean isNegotiating = false;
- public Boolean targetServerState = null;
- public String serverTransitionText = "";
- public UsageFragment usageFragment;
-
- public void setUsageFragment(UsageFragment fragment) {
- this.usageFragment = fragment;
- }
-
-
- /** ADFA-4520: last observed native-hotspot state, for the LOHS AND-recommendation. */
- public boolean isHotspotActive() { return serverController.isHotspotActive(); }
- private long pulseStartTime = 0;
-
- private ActivityResultLauncher requestPermissionsLauncher;
- private ActivityResultLauncher batteryOptLauncher;
-
- public boolean isReadingLogs = false;
- private final Handler sizeUpdateHandler = new Handler();
- private Runnable sizeUpdateRunnable;
-
- public ServerController serverController;
-
- // Load native C++ engine
- static {
- System.loadLibrary("termux");
- }
-
- /**
- * Dummy method to satisfy legacy fragments.
- * Since we are now a monolithic app with an embedded PRoot environment,
- * the host is always "installed".
- */
- public boolean isTermuxInstalled() {
- return true;
- }
-
- /** Notification-tap flag: open the full terminal directly (ADFA-4696). */
- public static final String EXTRA_OPEN_TERMINAL = "org.iiab.controller.OPEN_TERMINAL";
-
- /** Launched only to show the terminal (from the new UI): finish on close instead of
- * revealing the old dashboard. */
- public static final String EXTRA_TERMINAL_ONLY = "org.iiab.controller.TERMINAL_ONLY";
- private boolean terminalOnlyMode = false;
-
- private TerminalController terminalController;
-
- public void invalidateModuleStateTrust() {
- getSharedPreferences("iiab_queue_prefs", Context.MODE_PRIVATE)
- .edit()
- .putBoolean("is_module_state_trusted", false)
- .apply();
- }
-
- /**
- * ADFA-4466 Phase 1: single chokepoint for the server-alive polls so we can emit
- * server_started / server_stopped exactly on the transition (consent-gated, no-op
- * otherwise). server_stopped carries a coarse uptime bucket, never an exact duration.
- */
-
- public boolean isModuleStateTrusted() {
- return getSharedPreferences("iiab_queue_prefs", Context.MODE_PRIVATE)
- .getBoolean("is_module_state_trusted", true);
- }
-
- private final BroadcastReceiver logReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
-
- if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(action)) {
- String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE);
- addToLog(message);
- if (usageFragment != null) usageFragment.updateLogSizeUI();
- } else if (WatchdogService.ACTION_STATE_STARTED.equals(action)) {
- // Keep the visual sync pulse alive if the UI reloads while protected
- if (usageFragment != null) usageFragment.startFusionPulse();
- } else if (WatchdogService.ACTION_STATE_STOPPED.equals(action)) {
- // Service is down! Give it a visual margin, then stop the exit pulse.
- new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
- if (usageFragment != null) usageFragment.finalizeExitPulse();
- }, 1500);
- }
- }
- };
- // Listens for commands originating from the 'iiab' bash script in the host terminal
- private final BroadcastReceiver cliReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (action == null) return;
-
- switch (action) {
- case "org.iiab.ACTION_BAKE_IMAGE":
- // Delegated to bash, we do nothing here
- break;
- case "org.iiab.ACTION_BACKUP_ROOTFS":
- addToLog(getString(R.string.log_cli_backup_triggered));
- // triggerBackupProcess();
- break;
- case "org.iiab.ACTION_RESTORE_ROOTFS":
- addToLog(getString(R.string.log_cli_restore_triggered));
- // triggerRestoreProcess();
- break;
- case "org.iiab.ACTION_PREPARE_ROOTFS":
- // The terminal requested a clean boot environment
- File rootfsDir = new File(getFilesDir(), "rootfs/installed-rootfs/iiab");
- serverController.createFakeSysData(rootfsDir);
- break;
-// case "org.iiab.ACTION_UNLOCK_SDCARD":
-// File prootTmp = new File(getFilesDir(), "proot_tmp");
-//
-// runOnUiThread(() -> {
-// // 1. Ocultar físicamente la ventana de la terminal (BottomSheet)
-// // Esto mata CUALQUIER intento nativo de Termux de robar el foco o el teclado.
-// View bottomSheet = findViewById(R.id.terminal_bottom_sheet);
-// if (bottomSheet != null && bottomSheetBehavior != null) {
-// bottomSheetBehavior.setState(com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN);
-// }
-//
-// // 2. Por si acaso, quitar cualquier foco residual a nivel Java
-// if (terminalView != null) {
-// terminalView.setFocusable(false);
-// terminalView.setFocusableInTouchMode(false);
-// terminalView.clearFocus();
-// android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
-// if (imm != null) imm.hideSoftInputFromWindow(terminalView.getWindowToken(), 0);
-// }
-//
-// // Opcional: confirmación visual en Java
-// Toast.makeText(MainActivity.this, "Biometric Requested by Shell", Toast.LENGTH_SHORT).show();
-//
-// // 3. Lanzar la huella (con la terminal ya fuera del camino, Android tendrá la pantalla limpia)
-// BiometricHelper.prompt(MainActivity.this,
-// getString(R.string.terminal_auth_title),
-// getString(R.string.terminal_auth_subtitle),
-// new BiometricHelper.AuthCallback() {
-// @Override
-// public void onSuccess() {
-// try {
-// new File(prootTmp, ".auth_success").createNewFile();
-// addToLog(getString(R.string.log_cli_sdcard_granted));
-// } catch (Exception ignored) {
-// } finally {
-// restoreTerminalView();
-// }
-// }
-//
-// @Override
-// public void onFailed() {
-// try {
-// new File(prootTmp, ".auth_failed").createNewFile();
-// addToLog(getString(R.string.log_cli_sdcard_denied));
-// } catch (Exception ignored) {
-// } finally {
-// restoreTerminalView();
-// }
-// }
-//
-// // Método auxiliar para regresar todo a la normalidad
-// private void restoreTerminalView() {
-// // A. Restaurar el comportamiento de foco
-// if (terminalView != null) {
-// terminalView.setFocusable(true);
-// terminalView.setFocusableInTouchMode(true);
-// }
-//
-// // B. Volver a abrir el BottomSheet al 100% de la pantalla
-// if (bottomSheet != null && bottomSheetBehavior != null) {
-// // Fuerza la visibilidad por si acaso
-// if (bottomSheet.getVisibility() != View.VISIBLE) {
-// bottomSheet.setVisibility(View.VISIBLE);
-// }
-// bottomSheet.bringToFront();
-// bottomSheetBehavior.setState(com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED);
-//
-// // C. Devolver el foco a la terminal una vez que ya esté abierta
-// new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
-// if (terminalView != null) terminalView.requestFocus();
-// }, 300); // Darle tiempo a la animación de expansión
-// }
-// }
-// });
-// });
-// break;
- }
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Intercept launch and redirect to the legacy Setup Wizard when there is nothing to run.
- // ADFA-5137: asks the device rather than setup_complete, and says which mode it wants. The
- // old catch wrote the flag true so this branch would stop firing; with the flag gone there is
- // nothing to write, and nothing needs writing — the condition is re-derived every launch, so
- // a missing Activity stops being permanent state and goes back to being a log line.
- if (!terminalOnlyLaunch(getIntent())
- && !org.iiab.controller.system.data.SystemFactsReader.hereOrOnTheWay(this)) {
- try {
- startActivity(new Intent(this, SetupActivity.class)
- .putExtra(SetupActivity.EXTRA_WIZARD_MODE, true));
- finish();
- return; // We stop the execution of MainActivity right here
- } catch (android.content.ActivityNotFoundException e) {
- android.util.Log.w(TAG, "SetupActivity not found. Skipping initial setup.");
- }
- }
-
- if (savedInstanceState == null
- && new org.iiab.controller.feedback.crash.data.CrashReportStore(this).hasPending()) {
- startActivity(new Intent(this, org.iiab.controller.feedback.crash.presentation.CrashReportActivity.class));
- }
-
- prefs = new Preferences(this);
- setContentView(R.layout.main);
-
- // --- START TABS & VIEWPAGER ---
- tabLayout = findViewById(R.id.tab_layout);
- viewPager = findViewById(R.id.view_pager);
-
- MainPagerAdapter pagerAdapter = new MainPagerAdapter(this);
- viewPager.setAdapter(pagerAdapter);
- viewPager.setOffscreenPageLimit(3);
-
- new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
- switch (position) {
- case 0:
- tab.setText(R.string.tab_status);
- break;
- case 1:
- tab.setText(R.string.tab_usage);
- break;
- case 2:
- tab.setText(R.string.tab_deploy);
- break;
- case 3:
- tab.setText(R.string.tab_share);
- break;
- }
- }).attach();
-
- // ADFA-4538: draggable feedback FAB — present on all tabs (it lives in the activity,
- // above the ViewPager, so it persists across tab switches). Tap -> capture screenshot
- // -> open the feedback form tagged with the current tab.
- com.google.android.material.floatingactionbutton.FloatingActionButton fabFeedback =
- findViewById(R.id.fab_feedback);
- org.iiab.controller.feedback.presentation.FeedbackFab.attach(fabFeedback, () -> {
- int pos = viewPager.getCurrentItem();
- String screen = pos == 0 ? "status" : pos == 1 ? "usage" : pos == 2 ? "deploy"
- : pos == 3 ? "share" : "main";
- // ADFA-4932: shared with the redesign screens via FeedbackFab.sendFeedback.
- org.iiab.controller.feedback.presentation.FeedbackFab.sendFeedback(MainActivity.this, "main." + screen);
- });
-
- // --- TUTORIAL TAB DETECTOR ---
- viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageSelected(int position) {
- super.onPageSelected(position);
- if (position == 3) { // El índice 3 es la pestaña "Share"
- showShareTutorialIfNeeded();
- }
- }
- });
-
- // --- START: EASTER EGG & OTA LOGIC ---
- versionFooter = findViewById(R.id.version_text);
- setVersionFooter();
- updateController = new UpdateController(this);
-
- terminalController = new TerminalController(this, this);
- terminalController.bind();
- maybeOpenTerminalFromIntent(getIntent());
-
- // ADFA-4595: version footer — three gestures:
- // single tap -> check version / updates (OTA)
- // long-press -> show the main.version help tooltip
- // double-tap + hold -> open the hidden full terminal (double-tap = key, hold = confirm)
- versionFooter.setOnTouchListener(new View.OnTouchListener() {
- private final Handler handler = new Handler(android.os.Looper.getMainLooper());
- private final int longPressMs = android.view.ViewConfiguration.getLongPressTimeout();
- private final int doubleTapMs = android.view.ViewConfiguration.getDoubleTapTimeout();
- private long downTime = 0L;
- private long lastUpTime = 0L;
- private boolean secondTap = false;
- private boolean longFired = false;
- private Runnable longPress;
- private Runnable singleTap;
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- downTime = System.currentTimeMillis();
- secondTap = (downTime - lastUpTime) < doubleTapMs;
- longFired = false;
- if (singleTap != null) handler.removeCallbacks(singleTap);
- final boolean armedForTerminal = secondTap;
- longPress = () -> {
- longFired = true;
- if (armedForTerminal) {
- terminalController.openFullTerminal();
- } else {
- org.iiab.controller.help.TooltipManager.showTooltip(
- MainActivity.this, versionFooter,
- org.iiab.controller.help.TooltipCategory.K2GO,
- org.iiab.controller.help.TooltipTag.MAIN_VERSION);
- }
- };
- handler.postDelayed(longPress, longPressMs);
- return true;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (longPress != null) handler.removeCallbacks(longPress);
- long now = System.currentTimeMillis();
- boolean wasTap = !longFired && (now - downTime) < longPressMs;
- if (wasTap && !secondTap) {
- // Might be a single tap; confirm updates only if no second tap follows.
- lastUpTime = now;
- singleTap = () -> updateController.checkForUpdatesManual();
- handler.postDelayed(singleTap, doubleTapMs);
- } else {
- lastUpTime = wasTap ? now : 0L;
- }
- return true;
- }
- return false;
- }
- });
-
- viewPager.setCurrentItem(0, false);
-
- // 1. Initialize Result Launchers
- batteryOptLauncher = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- result -> {
- Log.d(TAG, "Returned from the battery settings screen");
- BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher);
- }
- );
-
- requestPermissionsLauncher = registerForActivityResult(
- new ActivityResultContracts.RequestMultiplePermissions(),
- result -> {
- for (Map.Entry entry : result.entrySet()) {
- if (entry.getKey().equals(Manifest.permission.POST_NOTIFICATIONS)) {
- addToLog(getString(entry.getValue() ? R.string.notif_perm_granted : R.string.notif_perm_denied));
- }
- }
- prepareVpn();
- }
- );
-
- themeToggle = findViewById(R.id.theme_toggle);
- btnSettings = findViewById(R.id.btn_settings);
- headerIcon = findViewById(R.id.header_icon);
- ImageButton btnShareQr = findViewById(R.id.btn_share_qr);
-
- // ADFA-4593: three-tier help — attach tier-1/2 tooltips (long-press) to native controls.
- org.iiab.controller.help.TooltipWiring.wireAll(getWindow().getDecorView());
-
- // Listeners
- themeToggle.setOnClickListener(v -> toggleTheme());
- btnSettings.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, SetupActivity.class)));
-
- // --- QR Share Button Logic ---
- btnShareQr.setOnClickListener(v -> {
- if (!ServerStateRepository.get().current().alive) {
- if (viewPager != null) {
- viewPager.setCurrentItem(1, true);
- }
- if (usageFragment != null) {
- new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
- usageFragment.highlightServerButton();
- }, 350);
- }
-
- // Rule 1: Server must be running
- Snackbars.make(findViewById(android.R.id.content), R.string.qr_error_no_server).show();
- return;
- }
- if (!serverController.isWifiActive() && !serverController.isHotspotActive()) {
- // Rule 2: At least one network must be active
- Snackbars.make(findViewById(android.R.id.content), R.string.qr_error_no_network).show();
- return;
- }
-
- // Launch the new QrActivity
- startActivity(new Intent(MainActivity.this, QrActivity.class));
- });
-
- applySavedTheme();
- updateUI();
-
- addToLog(getString(R.string.app_started));
- updateController.checkForUpdates(false);
-
- sizeUpdateRunnable = new Runnable() {
- @Override
- public void run() {
- if (usageFragment != null && usageFragment.isAdded())
- usageFragment.updateLogSizeUI();
- sizeUpdateHandler.postDelayed(this, 10000);
- }
- };
-
- serverController = new ServerController(this, this);
- serverController.start();
- }
-
- private void showBatterySnackbar() {
- View rootView = findViewById(android.R.id.content);
- Snackbar.make(rootView, R.string.battery_opt_denied, Snackbar.LENGTH_INDEFINITE)
- .setAction(R.string.fix_action, v -> BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher))
- .show();
- }
-
- private void initiatePermissionChain() {
- List permissions = new ArrayList<>();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- permissions.add(Manifest.permission.POST_NOTIFICATIONS);
- }
- }
-
- // ADDED CAMERA PERMISSION REQUEST
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
- permissions.add(Manifest.permission.CAMERA);
- }
-
- if (!permissions.isEmpty()) {
- requestPermissionsLauncher.launch(permissions.toArray(new String[0]));
- } else {
- prepareVpn();
- }
- }
-
-
- private void prepareVpn() {
- BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher);
- }
-
- public void startLogSizeUpdates() {
- sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable);
- sizeUpdateHandler.post(sizeUpdateRunnable);
- }
-
- public void stopLogSizeUpdates() {
- sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable);
- }
-
- @Override
- protected void onPause() {
- super.onPause();
-
- updateController.unregisterDownloadReceiver();
-
- stopLogSizeUpdates();
- serverController.onPause();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- updateController.registerDownloadReceiver();
- // Check permissions status
- updateHeaderIconsOpacity();
-
- // Check battery status whenever returning to the app
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) {
- Log.d(TAG, "onResume: Battery still optimized, showing warning");
- showBatterySnackbar();
- }
- }
- if (usageFragment != null && usageFragment.isLogVisible()) {
- startLogSizeUpdates();
- }
- serverController.onResume();
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- setIntent(intent);
- maybeOpenTerminalFromIntent(intent);
- }
-
- /**
- * ADFA-5137 (review): was this Activity launched only to show the terminal?
- *
- *
Asked before the first-run redirect above, because the order matters and it did not use to.
- * That redirect fired on {@code setup_complete}, which was true on any device that had ever
- * started an install, so a terminal launch never met it. Now it asks the disk — and with no
- * system, Settings → Terminal and the terminal's own keep-alive notification would land in the
- * legacy setup shell in wizard mode with Back blocked, having dropped the extras that said what
- * they came for.
- *
- *
Read here rather than deferring to {@code maybeOpenTerminalFromIntent}, which runs much later
- * in {@code onCreate}: the redirect happens first, so the question has to be answerable first.
- */
- private static boolean terminalOnlyLaunch(Intent intent) {
- // EXTRA_OPEN_TERMINAL alone, not paired with EXTRA_TERMINAL_ONLY: the redesign's Settings entry
- // sets both, but TerminalSessionService's keep-alive notification sets only the first, and both
- // came here to open a terminal. What decides the redirect is what the caller came for.
- return intent != null && intent.getBooleanExtra(EXTRA_OPEN_TERMINAL, false);
- }
-
- /** Open the full terminal when launched from its keep-alive notification (ADFA-4696). */
- private void maybeOpenTerminalFromIntent(Intent intent) {
- if (intent == null || terminalController == null) return;
- if (!intent.getBooleanExtra(EXTRA_OPEN_TERMINAL, false)) return;
- terminalOnlyMode = intent.getBooleanExtra(EXTRA_TERMINAL_ONLY, false);
- intent.removeExtra(EXTRA_OPEN_TERMINAL); // consume so it fires once
- intent.removeExtra(EXTRA_TERMINAL_ONLY);
- View root = findViewById(android.R.id.content);
- Runnable open = () -> {
- terminalController.openFullTerminal();
- if (terminalOnlyMode) {
- // ADFA-4987: opened from the redesign -> hide the legacy dashboard behind the sheet and
- // paint the root black, so a PARTIAL swipe-down reveals black (seamless with the terminal),
- // never the old STATUS/USAGE/INSTALL/SHARE UI. A full swipe still finish()es to the caller.
- //
- // TODO (terminal, follow-up): the black fill is a TEMPORARY stand-in. The complete fix is to
- // show the NEW UI behind the terminal (host the redesign Home/Library surface here) so a
- // partial swipe-down reveals the new UI instead of a black void. Left black for now because
- // embedding a redesign view inside this legacy MainActivity is a larger change. When the
- // terminal is next revisited, this is the spot. See ADFA-4987.
- View dash = findViewById(R.id.main_dashboard);
- if (dash != null) dash.setVisibility(View.GONE);
- View coord = findViewById(R.id.main_coordinator);
- if (coord != null) coord.setBackgroundColor(0xFF000000);
- attachTerminalOnlyFinish();
- } else {
- // ADFA-4987: defensive symmetry — a reused instance opened NOT in terminal-only mode must
- // show the dashboard and restore the theme background (undo any prior terminal-only chrome).
- View dash = findViewById(R.id.main_dashboard);
- if (dash != null) dash.setVisibility(View.VISIBLE);
- View coord = findViewById(R.id.main_coordinator);
- if (coord != null) {
- android.util.TypedValue tv = new android.util.TypedValue();
- getTheme().resolveAttribute(android.R.attr.windowBackground, tv, true);
- if (tv.resourceId != 0) coord.setBackgroundResource(tv.resourceId);
- else coord.setBackgroundColor(tv.data);
- }
- }
- };
- if (root != null) root.post(open);
- else open.run();
- }
-
- /** In terminal-only mode, hiding the terminal sheet returns to the caller (the new UI)
- * rather than exposing the old dashboard behind it. */
- private void attachTerminalOnlyFinish() {
- View sheet = findViewById(R.id.terminal_bottom_sheet);
- if (sheet == null) return;
- com.google.android.material.bottomsheet.BottomSheetBehavior b =
- com.google.android.material.bottomsheet.BottomSheetBehavior.from(sheet);
- // ADFA-4987: no intermediate COLLAPSED/peek stop in terminal-only mode. Without this the
- // swipe-down parks at the peek first, exposing the legacy dashboard behind the sheet; skipping
- // it means swipe-down goes straight to HIDDEN -> finish() -> back to the redesign caller.
- b.setHideable(true);
- b.setSkipCollapsed(true);
- b.addBottomSheetCallback(new com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@androidx.annotation.NonNull View bottomSheet, int newState) {
- if (newState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN) {
- finish();
- }
- }
- @Override
- public void onSlide(@androidx.annotation.NonNull View bottomSheet, float slideOffset) { }
- });
- }
-
- private void toggleTheme() {
- SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
- int currentMode = AppCompatDelegate.getDefaultNightMode();
- 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;
- sharedPref.edit().putInt("ui_mode", nextMode).apply();
- AppCompatDelegate.setDefaultNightMode(nextMode);
- updateThemeToggleButton(nextMode);
- }
-
- private void applySavedTheme() {
- SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
- int savedMode = sharedPref.getInt("ui_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
- AppCompatDelegate.setDefaultNightMode(savedMode);
- updateThemeToggleButton(savedMode);
- }
-
- private void updateThemeToggleButton(int mode) {
- if (mode == AppCompatDelegate.MODE_NIGHT_NO)
- themeToggle.setImageResource(R.drawable.ic_theme_dark);
- else if (mode == AppCompatDelegate.MODE_NIGHT_YES)
- themeToggle.setImageResource(R.drawable.ic_theme_light);
- else themeToggle.setImageResource(R.drawable.ic_theme_system);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(IIABWatchdog.ACTION_LOG_MESSAGE);
- filter.addAction(WatchdogService.ACTION_STATE_STARTED);
- filter.addAction(WatchdogService.ACTION_STATE_STOPPED);
-
- IntentFilter cliFilter = new IntentFilter();
- cliFilter.addAction("org.iiab.ACTION_BAKE_IMAGE");
- cliFilter.addAction("org.iiab.ACTION_BACKUP_ROOTFS");
- cliFilter.addAction("org.iiab.ACTION_RESTORE_ROOTFS");
- cliFilter.addAction("org.iiab.ACTION_PREPARE_ROOTFS");
-// cliFilter.addAction("org.iiab.ACTION_UNLOCK_SDCARD");
-
- // cliReceiver MUST be exported to receive commands from the system's 'am' binary
- ContextCompat.registerReceiver(this, cliReceiver, cliFilter, ContextCompat.RECEIVER_EXPORTED);
- ContextCompat.registerReceiver(this, logReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- try {
- unregisterReceiver(logReceiver);
- } catch (Exception e) {
- }
- try {
- unregisterReceiver(cliReceiver);
- } catch (Exception e) {
- }
- stopLogSizeUpdates();
- }
-
- @Override
- protected void onDestroy() {
- // ADFA-4696 (phase 2): release the terminal UI delegate so the session
- // store never holds this destroyed Activity. Running sessions are kept.
- if (terminalController != null) terminalController.detach();
- super.onDestroy();
- }
-
- @Override
- public void onClick(View view) {
- // Delegated
- }
-
- public void handleBrowseContentClick(View v) {
- if (!ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.qr_error_no_server).show();
- return;
- }
- String targetUrl = serverController.getCurrentTargetUrl();
- if (targetUrl != null) {
- Intent intent = new Intent(this, PortalActivity.class);
- intent.putExtra("TARGET_URL", targetUrl);
- startActivity(intent);
- }
- }
-
-
-
- public void updateUI() {
- if (usageFragment != null) {
- usageFragment.updateUI();
- }
- }
-
-
- public void handleServerLaunchClick(View v) {
- serverController.handleServerLaunchClick(v);
- }
-
- // --- ServerController.Host ------------------------------------------------
- @Override public void startFusionPulse() { if (usageFragment != null) usageFragment.startFusionPulse(); }
- @Override public void startExitPulse() { if (usageFragment != null) usageFragment.startExitPulse(); }
- @Override public void stopBtnProgress() { if (usageFragment != null) usageFragment.stopBtnProgress(); }
- @Override public void updateConnectivityLeds(boolean wifiOn, boolean hotspotOn) {
- if (usageFragment != null) usageFragment.updateConnectivityLeds(wifiOn, hotspotOn);
- }
- @Override public void refreshServerUi() { updateUIColorsAndVisibility(); }
- @Override public Boolean getTargetServerState() { return targetServerState; }
- @Override public void setTargetServerState(Boolean target) { targetServerState = target; }
- @Override public boolean isNegotiating() { return isNegotiating; }
-
- public void updateUIColorsAndVisibility() {
- if (usageFragment != null) {
- usageFragment.updateUIColorsAndVisibility();
- }
- }
-
- public void startTermuxEnvironmentVisible(String actionFlag) {
- android.util.Log.d(TAG, "Legacy Headless command ignored: " + actionFlag);
- }
-
- // --- TERMUX HEADLESS BRIDGE ---
- public void executeTermuxCommandHeadless(String actionFlag) {
- android.util.Log.d(TAG, "Legacy Headless command ignored: " + actionFlag);
- }
-
-
- public void savePrefs() {
- if (usageFragment != null) {
- usageFragment.savePrefsFromUI();
- }
- }
-
- public void addToLog(String message) {
- // ADFA-4640: single source of truth; the Usage console observes LogRepository.
- LogRepository.get().append(message);
- }
-
- /**
- * ADFA-4519: show a Snackbar anchored to the Activity CoordinatorLayout, never to a
- * fragment's NestedScrollView root. A ScrollView can host only one direct child, so a
- * Snackbar shown against it during an Activity recreation (e.g. a theme toggle) crashes
- * with "ScrollView can host only one direct child". The CoordinatorLayout is the intended
- * Snackbar host and has no such constraint. No-ops if the Activity is going away.
- */
- public void showSnackbar(CharSequence text) {
- if (isFinishing() || isDestroyed()) return;
- View anchor = findViewById(R.id.main_coordinator);
- if (anchor == null) anchor = findViewById(android.R.id.content);
- if (anchor == null) return;
- Snackbars.make(anchor, text).show();
- }
-
- private void setVersionFooter() {
- try {
- PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- String version = pInfo.versionName;
-
- String footerText = getString(R.string.version_footer_format, version);
-
- versionFooter.setText(footerText);
- } catch (PackageManager.NameNotFoundException e) {
- versionFooter.setText(getString(R.string.version_footer_fallback));
- }
- }
-
- // --- PERMISSION CHECKERS FOR UI OPACITY ---
-
- private void updateHeaderIconsOpacity() {
- // Verify only the 4 native permissions required by our new monolithic architecture
- boolean hasAllPerms = hasNotifPermission() && hasBatteryPermission() && hasStoragePermission();
-
- float targetAlpha = hasAllPerms ? 1.0f : 0.4f;
-
- if (btnSettings != null) btnSettings.setAlpha(targetAlpha);
- if (headerIcon != null) headerIcon.setAlpha(targetAlpha);
- }
-
- 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 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;
- }
-
- private boolean hasStoragePermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- return Environment.isExternalStorageManager();
- } else {
- return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
- }
- }
-
-
- public void vibrateDevice() {
- android.os.Vibrator v = (android.os.Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
- if (v != null && v.hasVibrator()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- v.vibrate(android.os.VibrationEffect.createOneShot(50, android.os.VibrationEffect.DEFAULT_AMPLITUDE));
- } else {
- v.vibrate(50);
- }
- }
- }
-
- // WATCHDOG PROTECTION UTILS
- public void enableSystemProtection() {
- Intent intent = new Intent(this, WatchdogService.class);
- intent.setAction(WatchdogService.ACTION_START);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- startForegroundService(intent);
- } else {
- startService(intent);
- }
- }
-
- public void disableSystemProtection() {
- Intent intent = new Intent(this, WatchdogService.class);
- intent.setAction(WatchdogService.ACTION_STOP);
- startService(intent);
- }
-
- // TUTORIAL OVERLAY LOGIC
- private void showShareTutorialIfNeeded() {
- SharedPreferences internalPrefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
- boolean hideTutorial = internalPrefs.getBoolean("hide_share_tutorial", false);
-
- if (hideTutorial) return;
-
- android.view.ViewGroup root = findViewById(android.R.id.content);
- if (root.findViewById(R.id.tutorial_root) != null) return;
-
- View tutorialView = getLayoutInflater().inflate(R.layout.overlay_tutorial_share, root, false);
- root.addView(tutorialView);
-
- // --- Clone QR BUTTON ---
- ImageButton realBtn = findViewById(R.id.btn_share_qr);
- android.widget.ImageView fakeBtn = tutorialView.findViewById(R.id.fake_share_btn);
- View bubbleContainer = tutorialView.findViewById(R.id.bubble_light_container);
-
- if (realBtn != null && fakeBtn != null) {
- // We copy the icon you are currently using
- fakeBtn.setImageDrawable(realBtn.getDrawable());
-
- fakeBtn.setOnClickListener(v -> {
- tutorialView.animate()
- .alpha(0f)
- .setDuration(300)
- .withEndAction(() -> {
- root.removeView(tutorialView);
- realBtn.performClick();
- })
- .start();
- });
-
- // We wait for the screen to finish drawing to obtain precise coordinates
- tutorialView.post(() -> {
- // 1. Obtain real coordinates by mitigating the Offset of the Status Bar
- int[] rootLoc = new int[2];
- tutorialView.getLocationInWindow(rootLoc);
-
- int[] btnLoc = new int[2];
- realBtn.getLocationInWindow(btnLoc);
-
- float exactX = btnLoc[0] - rootLoc[0];
- float exactY = btnLoc[1] - rootLoc[1];
-
- // 2. Position the Clone Button
- fakeBtn.setX(exactX);
- fakeBtn.setY(exactY);
- fakeBtn.getLayoutParams().width = realBtn.getWidth();
- fakeBtn.getLayoutParams().height = realBtn.getHeight();
- fakeBtn.requestLayout();
-
- // 3. Align the needle EXACTLY to the center of the cloned button
- View needle = tutorialView.findViewById(R.id.pointer_needle);
- float needleX = exactX + (realBtn.getWidth() / 2f) - (needle.getWidth() / 2f);
- float needleY = exactY + realBtn.getHeight() - (needle.getHeight() / 2f);
- needle.setX(needleX);
- needle.setY(needleY);
-
- // 4. Position the blue bubble to align with the needle
- bubbleContainer.setY(needleY + (needle.getHeight() / 2f) - 6);
- });
- }
-
- // Entry animation
- tutorialView.animate().alpha(1f).setDuration(400).start();
-
- android.widget.Button btnGotIt = tutorialView.findViewById(R.id.btn_got_it);
- android.widget.CheckBox chkDontShow = tutorialView.findViewById(R.id.chk_dont_show_again);
-
- btnGotIt.setOnClickListener(v -> {
- if (chkDontShow.isChecked()) {
- internalPrefs.edit().putBoolean("hide_share_tutorial", true).apply();
- }
-
- tutorialView.animate()
- .alpha(0f)
- .setDuration(300)
- .withEndAction(() -> root.removeView(tutorialView))
- .start();
- });
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java b/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java
deleted file mode 100644
index 47619a050..000000000
--- a/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * ============================================================================
- * Name : MainPagerAdapter.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Main Pager Adapter
- * ============================================================================
- */
-package org.iiab.controller;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-public class MainPagerAdapter extends FragmentStateAdapter {
-
- public MainPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
- super(fragmentActivity);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- switch (position) {
- case 0:
- return new DashboardFragment();
- case 1:
- return new UsageFragment();
- case 2:
- return new DeployFragment();
- case 3:
- return new SyncFragment();
- default:
- return new DashboardFragment();
- }
- }
-
- @Override
- public int getItemCount() {
- return 4;
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/MultiResourceGaugeView.java b/controller/app/src/main/java/org/iiab/controller/MultiResourceGaugeView.java
deleted file mode 100644
index 8c9da37ec..000000000
--- a/controller/app/src/main/java/org/iiab/controller/MultiResourceGaugeView.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * ============================================================================
- * Name : MultiResourceGaugeView.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Custom view for displaying multiple animated resource gauges
- * ============================================================================
- */
-
-package org.iiab.controller;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.DecelerateInterpolator;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class MultiResourceGaugeView extends View {
-
- private Paint arcPaint, bgArcPaint, percentPaint, valuePaint, titlePaint;
- private RectF rectF;
- private float animatedFactor = 0f;
-
- private String titleText = "Projected";
- private String centerText = "0%";
- private String bottomText = "-- GB";
-
- // Structure to hold the pie segments
- public static class Segment {
- public float percentage;
- public int color;
-
- public Segment(float percentage, int color) {
- this.percentage = percentage;
- this.color = color;
- }
- }
-
- private List segments = new ArrayList<>();
-
- public MultiResourceGaugeView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
-
- private void init(Context context) {
- setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- rectF = new RectF();
-
- // 1. Background arc
- bgArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- bgArcPaint.setStyle(Paint.Style.STROKE);
- bgArcPaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.dash_bar_bg));
- bgArcPaint.setStrokeCap(Paint.Cap.ROUND);
-
- arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- arcPaint.setStyle(Paint.Style.STROKE);
- arcPaint.setStrokeWidth(20f);
- arcPaint.setStrokeCap(Paint.Cap.ROUND);
-
- // 2. Central Text (0.0G) - Now use the semantic variable
- percentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- percentPaint.setTextAlign(Paint.Align.CENTER);
- percentPaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.dash_text_inverted));
- percentPaint.setFakeBoldText(true);
-
- valuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- valuePaint.setTextAlign(Paint.Align.CENTER);
- valuePaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.dash_text_secondary));
-
- // 3. Title ("Storage" / "Projected")
- titlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- titlePaint.setTextAlign(Paint.Align.CENTER);
- titlePaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.dash_text_gauge_title));
- titlePaint.setFakeBoldText(true);
-
- // Font Orbitron
- try {
- Typeface orbitron = androidx.core.content.res.ResourcesCompat.getFont(context, R.font.orbitron);
- percentPaint.setTypeface(orbitron);
- valuePaint.setTypeface(orbitron);
- } catch (Exception e) {
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- int desiredWidth = widthSize;
- int desiredHeight = heightSize;
-
- if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
- desiredHeight = desiredWidth;
- }
- setMeasuredDimension(desiredWidth, desiredHeight);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- float size = Math.min(w, h);
- float strokeW = size * 0.07f;
- bgArcPaint.setStrokeWidth(strokeW);
- arcPaint.setStrokeWidth(strokeW);
-
- float cx = w / 2f;
- float cy = h / 2f;
- float radius = (size / 2f) - (strokeW + 5f);
- rectF.set(cx - radius, cy - radius, cx + radius, cy + radius);
-
- percentPaint.setTextSize(size / 5.0f);
- valuePaint.setTextSize(size / 13f);
- titlePaint.setTextSize(size / 11f);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- // Draw the entire dark background
- canvas.drawArc(rectF, 135, 270, false, bgArcPaint);
-
- float startAngle = 135f;
-
- // Draw each segment sequentially
- for (Segment segment : segments) {
- float targetSweep = 270f * (segment.percentage / 100f);
- float currentSweep = targetSweep * animatedFactor; // Progressive animation
-
- if (currentSweep > 0) {
- arcPaint.setColor(segment.color);
- arcPaint.setShadowLayer(25f, 0f, 0f, segment.color); // Dynamic glow based on color
- canvas.drawArc(rectF, startAngle, currentSweep, false, arcPaint);
- startAngle += currentSweep; // The next color starts where this one ends
- }
- }
-
- float centerX = getWidth() / 2f;
- float centerY = getHeight() / 2f;
-
- canvas.drawText(titleText, centerX, centerY - (percentPaint.getTextSize() * 0.85f), titlePaint);
- canvas.drawText(centerText, centerX, centerY + (percentPaint.getTextSize() * 0.25f), percentPaint);
- canvas.drawText(bottomText, centerX, centerY + (percentPaint.getTextSize() * 0.75f), valuePaint);
- }
-
- // Main method to inject data
- public void updateData(List newSegments, String centerTxt, int centerTxtColor, String bottomTxt, String title) {
- this.titleText = title;
- this.centerText = centerTxt;
- this.bottomText = bottomTxt;
-
- // Apply color and glow to the central text
- this.percentPaint.setColor(centerTxtColor);
-
- this.segments.clear();
- this.segments.addAll(newSegments);
-
- // Smooth fill animation
- ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
- animator.setDuration(900);
- animator.setInterpolator(new DecelerateInterpolator());
- animator.addUpdateListener(animation -> {
- animatedFactor = (float) animation.getAnimatedValue();
- invalidate();
- });
- animator.start();
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/ProgressButton.java b/controller/app/src/main/java/org/iiab/controller/ProgressButton.java
deleted file mode 100644
index b30b72f66..000000000
--- a/controller/app/src/main/java/org/iiab/controller/ProgressButton.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * ============================================================================
- * Name : ProgressButton.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Button animation helper
- * ============================================================================
- */
-package org.iiab.controller;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.graphics.Path;
-import android.util.AttributeSet;
-
-import androidx.core.content.ContextCompat;
-import androidx.appcompat.widget.AppCompatButton;
-
-public class ProgressButton extends AppCompatButton {
-
- private Paint progressPaint;
- private Paint progressBackgroundPaint;
-
- private int progressColor;
- private int progressBackgroundColor;
- private Path clipPath;
- private RectF rectF;
- private float cornerRadius;
- private int progressHeight;
-
- // Animation variables
- private float currentProgress = 0f;
- private boolean isRunning = false;
- private ValueAnimator animator;
-
- public ProgressButton(Context context) {
- super(context);
- init(context, null);
- }
-
- public ProgressButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs);
- }
-
- public ProgressButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init(context, attrs);
- }
-
- private void init(Context context, AttributeSet attrs) {
- if (attrs != null) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressButton, 0, 0);
- try {
- progressColor = a.getColor(R.styleable.ProgressButton_progressButtonColor, ContextCompat.getColor(context, R.color.btn_danger));
- progressBackgroundColor = a.getColor(R.styleable.ProgressButton_progressButtonBackgroundColor, Color.parseColor("#44888888"));
- progressHeight = a.getDimensionPixelSize(R.styleable.ProgressButton_progressButtonHeight, (int) (6 * getResources().getDisplayMetrics().density));
- // Note: We no longer read 'duration' from XML because the animation is infinite.
- } finally {
- a.recycle();
- }
- } else {
- // Safe defaults
- progressColor = ContextCompat.getColor(context, R.color.btn_danger);
- progressBackgroundColor = Color.parseColor("#44888888");
- progressHeight = (int) (6 * getResources().getDisplayMetrics().density);
- }
-
- progressPaint = new Paint();
- progressPaint.setColor(progressColor);
- progressPaint.setStyle(Paint.Style.FILL);
-
- progressBackgroundPaint = new Paint();
- progressBackgroundPaint.setColor(progressBackgroundColor);
- progressBackgroundPaint.setStyle(Paint.Style.FILL);
-
- // Initialize clipping path variables
- clipPath = new Path();
- rectF = new RectF();
- cornerRadius = 8 * getResources().getDisplayMetrics().density;
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- // Draw the background and text first
- super.onDraw(canvas);
-
- // Draw the progress bar constrained by the button's rounded corners
- if (progressHeight > 0 && isRunning) {
- int buttonWidth = getWidth();
- int buttonHeight = getHeight();
-
- // Calculate width based on the current animated float (0.0f to 1.0f)
- int progressWidth = (int) (buttonWidth * currentProgress);
-
- // 1. Prepare the rounded mask (matches the button's bounds)
- rectF.set(0, 0, buttonWidth, buttonHeight);
- clipPath.reset();
- clipPath.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW);
-
- // 2. Save canvas state and apply the mask
- canvas.save();
- canvas.clipPath(clipPath);
-
- // 3. Draw the tracks
- canvas.drawRect(0, buttonHeight - progressHeight, buttonWidth, buttonHeight, progressBackgroundPaint);
- canvas.drawRect(0, buttonHeight - progressHeight, progressWidth, buttonHeight, progressPaint);
-
- // 4. Restore canvas
- canvas.restore();
- }
- }
-
- /**
- * Starts an infinite cyclic animation (fills and empties the bar).
- * Disables the button to prevent spam clicks.
- */
- public void startProgress() {
- if (isRunning) return;
- isRunning = true;
- setEnabled(false); // Lock the button immediately
-
- // Create an animator that goes from 0.0 to 1.0 (empty to full)
- animator = ValueAnimator.ofFloat(0f, 1f);
- animator.setDuration(1200); // 1.2 seconds per sweep
- animator.setRepeatMode(ValueAnimator.REVERSE); // Fill up, then empty down
- animator.setRepeatCount(ValueAnimator.INFINITE); // Never stop until commanded
-
- animator.addUpdateListener(animation -> {
- currentProgress = (float) animation.getAnimatedValue();
- invalidate(); // Force redraw on every frame
- });
-
- animator.start();
- }
-
- /**
- * Stops the animation, clears the bar, and unlocks the button.
- * To be called by the Controller when the backend confirms the state change.
- */
- public void stopProgress() {
- if (animator != null && animator.isRunning()) {
- animator.cancel();
- }
- isRunning = false;
- setEnabled(true); // Unlock button
- currentProgress = 0f; // Reset width
- invalidate(); // Clear the bar visually
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/ResourceGaugeView.java b/controller/app/src/main/java/org/iiab/controller/ResourceGaugeView.java
deleted file mode 100644
index 01c304482..000000000
--- a/controller/app/src/main/java/org/iiab/controller/ResourceGaugeView.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * ============================================================================
- * Name : ResourceGaugeView.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Custom view for displaying animated resource gauges
- * ============================================================================
- */
-package org.iiab.controller;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.DecelerateInterpolator;
-
-public class ResourceGaugeView extends View {
-
- private Paint arcPaint, bgArcPaint, percentPaint, valuePaint, titlePaint;
- private RectF rectF;
- private float currentProgress = 0f;
- private float animatedProgress = 0f;
-
- private String titleText = "Resource";
- private String centerText = "0%";
- private String bottomText = "-- / --";
-
- private int currentColor;
-
- public ResourceGaugeView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
-
- private void init(Context context) {
- setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- rectF = new RectF();
- currentColor = androidx.core.content.ContextCompat.getColor(context, R.color.status_success);
-
- bgArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- bgArcPaint.setStyle(Paint.Style.STROKE);
- bgArcPaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.chart_track));
- bgArcPaint.setStrokeCap(Paint.Cap.ROUND);
- bgArcPaint.setPathEffect(null);
-
- arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- arcPaint.setStyle(Paint.Style.STROKE);
- arcPaint.setStrokeWidth(20f);
- arcPaint.setStrokeCap(Paint.Cap.ROUND);
-
- // Light up first glow
- arcPaint.setShadowLayer(25f, 0f, 0f, currentColor);
-
- percentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- percentPaint.setTextAlign(Paint.Align.CENTER);
- percentPaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.text_primary));
- percentPaint.setFakeBoldText(true);
-
- valuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- valuePaint.setTextAlign(Paint.Align.CENTER);
- valuePaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.text_secondary));
-
- titlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- titlePaint.setTextAlign(Paint.Align.CENTER);
- titlePaint.setColor(androidx.core.content.ContextCompat.getColor(context, R.color.text_secondary));
- titlePaint.setFakeBoldText(true);
-
- // LOAD AND APPLY ORBITRON
- try {
- Typeface orbitron = androidx.core.content.res.ResourcesCompat.getFont(context, R.font.orbitron);
- percentPaint.setTypeface(orbitron);
- valuePaint.setTypeface(orbitron);
- } catch (Exception e) {
- // Silent fallback in case Android cannot find the font
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- int desiredWidth = widthSize;
- int desiredHeight = heightSize;
-
- // PORTRAIT ANIMATION
- if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
- desiredHeight = desiredWidth;
- }
-
- setMeasuredDimension(desiredWidth, desiredHeight);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
-
- // LANDSCAPE ANIMATION (Perfectly Centered)
- float size = Math.min(w, h);
-
- float strokeW = size * 0.07f;
- bgArcPaint.setStrokeWidth(strokeW);
- arcPaint.setStrokeWidth(strokeW);
-
- float cx = w / 2f;
- float cy = h / 2f;
-
- // Calculate the radius of the circle
- float radius = (size / 2f) - (strokeW + 5f);
-
- // Anchor the drawing rectangle exactly in the center of the view
- rectF.set(cx - radius, cy - radius, cx + radius, cy + radius);
-
- // Dynamic scale of fonts
- percentPaint.setTextSize(size / 5.0f);
- valuePaint.setTextSize(size / 15f);
- titlePaint.setTextSize(size / 11f);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- canvas.drawArc(rectF, 135, 270, false, bgArcPaint);
-
- arcPaint.setColor(currentColor);
- float sweepAngle = 270 * (animatedProgress / 100f);
- canvas.drawArc(rectF, 135, sweepAngle, false, arcPaint);
-
- float centerX = getWidth() / 2f;
- float centerY = getHeight() / 2f;
-
- // VERTICAL COMPACTION OF TEXTS
- canvas.drawText(titleText, centerX, centerY - (percentPaint.getTextSize() * 0.85f), titlePaint);
- canvas.drawText(centerText, centerX, centerY + (percentPaint.getTextSize() * 0.25f), percentPaint);
- canvas.drawText(bottomText, centerX, centerY + (percentPaint.getTextSize() * 0.75f), valuePaint);
- }
-
- // Force update animation on touch
- public void triggerAnimation() {
- ValueAnimator animator = ValueAnimator.ofFloat(0f, currentProgress);
- animator.setDuration(1000);
- animator.setInterpolator(new DecelerateInterpolator());
- animator.addUpdateListener(animation -> {
- animatedProgress = (float) animation.getAnimatedValue();
- invalidate();
- });
- animator.start();
- }
-
- // Main method for updating the animated bar with explicit color definition
- public void updateData(float progress, String values, String title, int customColor) {
- this.titleText = title;
- this.currentColor = customColor;
- this.bottomText = values;
- this.centerText = (int) progress + "%";
-
- arcPaint.setShadowLayer(25f, 0f, 0f, currentColor);
-
- // Avoid unnecessary animations if the value is the same
- if (this.currentProgress == progress) {
- invalidate();
- return;
- }
-
- // Smooth fill animation
- ValueAnimator animator = ValueAnimator.ofFloat(this.currentProgress, progress);
- animator.setDuration(800);
- animator.setInterpolator(new DecelerateInterpolator());
- animator.addUpdateListener(animation -> {
- animatedProgress = (float) animation.getAnimatedValue();
- invalidate();
- });
- animator.start();
-
- this.currentProgress = progress;
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/ServerLogView.java b/controller/app/src/main/java/org/iiab/controller/ServerLogView.java
deleted file mode 100644
index e5eb3fa00..000000000
--- a/controller/app/src/main/java/org/iiab/controller/ServerLogView.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * ============================================================================
- * Name : ServerLogView.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : ADFA-4640 — server-log console with terminal-style scrolling:
- * sticky read position, follow/tail (less +F), a jump-to-newest
- * affordance, a grabbable custom scrollbar (tap-to-jump + drag),
- * and a bounded buffer. Uses a custom scrollbar instead of the
- * AndroidX fast-scroller (which was not tap-jumpable, rendered a
- * second bar next to the native one, and was hard to grab).
- * ============================================================================
- */
-package org.iiab.controller;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ServerLogView extends FrameLayout {
-
- /** Keep at most this many lines in memory. */
- private static final int MAX_LINES = 8000;
-
- private RecyclerView recycler;
- private LinearLayoutManager layoutManager;
- private LogAdapter adapter;
- private View jumpLatest;
- private View scrollbar;
- private View thumb;
- private boolean following = true;
-
- public ServerLogView(@NonNull Context context) { this(context, null); }
-
- public ServerLogView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }
-
- @SuppressLint("ClickableViewAccessibility")
- public ServerLogView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- LayoutInflater.from(context).inflate(R.layout.view_server_log, this, true);
- recycler = findViewById(R.id.log_recycler);
- jumpLatest = findViewById(R.id.log_jump_latest);
- scrollbar = findViewById(R.id.log_scrollbar);
- thumb = findViewById(R.id.log_scroll_thumb);
-
- layoutManager = new LinearLayoutManager(context);
- layoutManager.setStackFromEnd(true); // pin content to the bottom (newest) like a log
- recycler.setLayoutManager(layoutManager);
- adapter = new LogAdapter();
- recycler.setAdapter(adapter);
-
- // Own the vertical drag while nested in the page ScrollView.
- recycler.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
- int action = e.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
- disallowParentIntercept(true);
- } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- disallowParentIntercept(false);
- }
- return false;
- }
- });
-
- recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
- // Only a user-driven scroll (drag/fling) changes follow state; programmatic
- // scrolls (append tail, jump) are instant (IDLE) and must not detach.
- if (rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
- following = isAtBottom();
- jumpLatest.setVisibility(following ? GONE : VISIBLE);
- }
- updateThumb();
- }
- });
-
- // Grabbable scrollbar: tap anywhere to jump there, drag to move across thousands of lines.
- scrollbar.setOnTouchListener((v, e) -> {
- int trackH = scrollbar.getHeight();
- int count = adapter.getItemCount();
- if (trackH <= 0 || count == 0) return false;
- int action = e.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
- disallowParentIntercept(true);
- float frac = clamp01(e.getY() / trackH);
- int target = Math.round(frac * (count - 1));
- layoutManager.scrollToPositionWithOffset(target, 0);
- following = frac >= 0.995f;
- jumpLatest.setVisibility(following ? GONE : VISIBLE);
- recycler.post(this::updateThumb);
- return true;
- } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- disallowParentIntercept(false);
- following = isAtBottom();
- jumpLatest.setVisibility(following ? GONE : VISIBLE);
- return true;
- }
- return false;
- });
-
- jumpLatest.setOnClickListener(v -> {
- following = true;
- scrollToEnd();
- jumpLatest.setVisibility(GONE);
- });
- jumpLatest.setVisibility(GONE);
- }
-
- private void disallowParentIntercept(boolean disallow) {
- ViewGroup p = (ViewGroup) getParent();
- if (p != null) p.requestDisallowInterceptTouchEvent(disallow);
- }
-
- private static float clamp01(float v) {
- return v < 0f ? 0f : (v > 1f ? 1f : v);
- }
-
- /** At the bottom when the list can no longer scroll further down. */
- private boolean isAtBottom() {
- return !recycler.canScrollVertically(1);
- }
-
- private void scrollToEnd() {
- int n = adapter.getItemCount();
- if (n > 0) {
- recycler.scrollToPosition(n - 1);
- recycler.post(this::updateThumb);
- }
- }
-
- /** Position/size the custom scrollbar thumb from the RecyclerView scroll metrics. */
- private void updateThumb() {
- int trackH = scrollbar.getHeight();
- if (trackH <= 0) return;
- int range = recycler.computeVerticalScrollRange();
- int extent = recycler.computeVerticalScrollExtent();
- int offset = recycler.computeVerticalScrollOffset();
- if (adapter.getItemCount() == 0 || range <= extent) {
- scrollbar.setVisibility(GONE);
- return;
- }
- scrollbar.setVisibility(VISIBLE);
- int minThumb = Math.round(40f * getResources().getDisplayMetrics().density);
- int thumbH = (int) ((extent / (float) range) * trackH);
- if (thumbH < minThumb) thumbH = minThumb;
- if (thumbH > trackH) thumbH = trackH;
- ViewGroup.LayoutParams lp = thumb.getLayoutParams();
- if (lp.height != thumbH) {
- lp.height = thumbH;
- thumb.setLayoutParams(lp);
- }
- float denom = range - extent;
- float frac = denom > 0 ? offset / denom : 0f;
- thumb.setTranslationY(clamp01(frac) * (trackH - thumbH));
- }
-
- /** Append one line (no trailing newline). Auto-scrolls only while following the tail. */
- public void append(String line) {
- if (line == null) return;
- adapter.add(line);
- if (following) {
- scrollToEnd();
- } else {
- jumpLatest.setVisibility(VISIBLE);
- recycler.post(this::updateThumb);
- }
- }
-
- /** Replace all content from a newline-delimited string; sticks to the end. */
- public void setContent(String content) {
- List lines = new ArrayList<>();
- if (content != null && !content.isEmpty()) {
- String[] parts = content.split("\n", -1);
- for (String p : parts) lines.add(p);
- if (!lines.isEmpty() && lines.get(lines.size() - 1).isEmpty()) {
- lines.remove(lines.size() - 1);
- }
- }
- adapter.setAll(lines);
- following = true;
- scrollToEnd();
- jumpLatest.setVisibility(GONE);
- }
-
- /** Clear all lines. */
- public void clear() {
- adapter.setAll(new ArrayList<>());
- following = true;
- jumpLatest.setVisibility(GONE);
- recycler.post(this::updateThumb);
- }
-
- /** All current lines joined with '\n' (for copy-to-clipboard). */
- public String getContent() {
- return TextUtils.join("\n", adapter.lines);
- }
-
- // ---------------------------------------------------------------------
-
- private static final class LogAdapter extends RecyclerView.Adapter {
-
- final ArrayList lines = new ArrayList<>();
-
- void add(String line) {
- lines.add(line);
- int over = lines.size() - MAX_LINES;
- if (over > 0) {
- lines.subList(0, over).clear();
- notifyDataSetChanged();
- } else {
- notifyItemInserted(lines.size() - 1);
- }
- }
-
- void setAll(List newLines) {
- lines.clear();
- lines.addAll(newLines);
- if (lines.size() > MAX_LINES) {
- lines.subList(0, lines.size() - MAX_LINES).clear();
- }
- notifyDataSetChanged();
- }
-
- @NonNull
- @Override
- public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- TextView tv = (TextView) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.item_log_line, parent, false);
- return new Holder(tv);
- }
-
- @Override
- public void onBindViewHolder(@NonNull Holder holder, int position) {
- holder.text.setText(lines.get(position));
- }
-
- @Override
- public int getItemCount() {
- return lines.size();
- }
-
- static final class Holder extends RecyclerView.ViewHolder {
- final TextView text;
- Holder(@NonNull TextView itemView) {
- super(itemView);
- this.text = itemView;
- }
- }
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/SyncFragment.java b/controller/app/src/main/java/org/iiab/controller/SyncFragment.java
deleted file mode 100644
index 22be90379..000000000
--- a/controller/app/src/main/java/org/iiab/controller/SyncFragment.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * ============================================================================
- * Name : SyncFragment.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Fragment to handle P2P/P2M syncing and App sharing
- * ============================================================================
- */
-
-package org.iiab.controller;
-
-import android.os.Bundle;
-import android.content.Intent;
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.RadioGroup;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import org.iiab.controller.ui.dialog.BrandDialog;
-import androidx.fragment.app.Fragment;
-
-import androidx.lifecycle.ViewModelProvider;
-import androidx.activity.result.ActivityResultLauncher;
-
-import com.journeyapps.barcodescanner.ScanContract;
-import com.journeyapps.barcodescanner.ScanOptions;
-
-
-public class SyncFragment extends Fragment implements org.iiab.controller.sync.presentation.ArchCheckHost,
- org.iiab.controller.sync.presentation.ShareHost,
- org.iiab.controller.sync.presentation.ReceiveHost {
-
- private static final String TAG = "IIAB-SyncFragment";
- // S16: name the ADB-optimization prefs/keys (shared with the ADB-share tab).
- private static final String ADB_PREFS = "iiab_adb_prefs";
- private static final String PREF_FOCUS_ADB = "focus_adb";
-
- // ADFA-4506: arch labels + compatibility dialogs carved into a controller.
- private final org.iiab.controller.sync.presentation.ArchCheckController archCheckController =
- new org.iiab.controller.sync.presentation.ArchCheckController(this, this);
-
- // ADFA-4506: the Share area (rsync daemon + APK server) carved into a controller.
- private final org.iiab.controller.sync.presentation.ShareController shareController =
- new org.iiab.controller.sync.presentation.ShareController(this, this);
-
- // ADFA-4506: the Receive flow (scan/probe/dry-run/transfer) carved into a controller.
- private final org.iiab.controller.sync.presentation.ReceiveController receiveController =
- new org.iiab.controller.sync.presentation.ReceiveController(this, this);
-
- private RadioGroup rgSyncMode;
- private LinearLayout containerShare, containerReceive;
-
- // Managers
- private org.iiab.controller.sync.transport.TransportEngine transport;
- private org.iiab.controller.sync.presentation.SyncStateViewModel syncVm; // 3b-2: survives recreation
-
- // Share config (rsync port/user/module/apk-port) — used by the Receive probe;
- // the Share area itself is owned by ShareController.
- private final org.iiab.controller.sync.domain.ShareConfig shareConfig = org.iiab.controller.sync.domain.ShareConfig.defaults();
-
- // Scanner Launcher
- private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new ScanContract(), result -> {
- if (result.getContents() != null) {
- receiveController.handleScannedData(result.getContents());
- } else {
- Toast.makeText(getContext(), getString(R.string.cancel), Toast.LENGTH_SHORT).show();
- }
- });
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_sync, container, false);
- syncVm = new ViewModelProvider(requireActivity()).get(org.iiab.controller.sync.presentation.SyncStateViewModel.class);
- transport = syncVm.getTransport();
-
- shareController.bind(view, transport, shareConfig);
- receiveController.bind(view, syncVm, shareConfig);
-
- rgSyncMode = view.findViewById(R.id.rg_sync_mode);
- containerShare = view.findViewById(R.id.container_share);
- containerReceive = view.findViewById(R.id.container_receive);
-
- TextView txtHostArchLabel = view.findViewById(R.id.txt_host_arch_label);
- TextView txtGuestArchLabel = view.findViewById(R.id.txt_guest_arch_label);
-
- // ADFA-4506: arch labels + compatibility dialogs are owned by ArchCheckController.
- archCheckController.bind(txtHostArchLabel, txtGuestArchLabel);
- archCheckController.applyStaticLabels();
- archCheckController.updateArchLabelsVisibility();
- setupToggleLogic();
-
- return view;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- org.iiab.controller.help.TooltipWiring.wireAll(view);
- // 3b-2: re-bind the rsync transfer progress after any recreation (theme toggle).
- org.iiab.controller.sync.presentation.SyncProgressRepository.get().state().observe(getViewLifecycleOwner(), receiveController::renderTransfer);
- }
-
- private void setupToggleLogic() {
- rgSyncMode.setOnCheckedChangeListener((group, checkedId) -> {
- if (checkedId == R.id.rb_mode_share) {
- containerShare.setVisibility(View.VISIBLE);
- containerReceive.setVisibility(View.GONE);
- } else if (checkedId == R.id.rb_mode_receive) {
- containerShare.setVisibility(View.GONE);
- containerReceive.setVisibility(View.VISIBLE);
- }
- archCheckController.updateArchLabelsVisibility();
- });
- }
-
- /** Renders the transfer state from SyncProgressRepository; re-binds after recreation (3b-2). */
-
- /** Forces the Share tab into receive mode so the transfer progress is visible after a
- * recreation (the mode toggle resets otherwise). 3b-2. */
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- // 3b-2: keep an in-flight transfer alive across a configuration change (theme toggle);
- // the transport lives in the Activity-scoped ViewModel and the UI re-binds on recreate.
- if (getActivity() != null && getActivity().isChangingConfigurations()
- && org.iiab.controller.sync.presentation.SyncProgressRepository.get().isActive()) {
- return;
- }
- // ADFA-4960: the transport is now process-global (shared with the redesign clone). Don't stop it
- // — nor drop the network binding — while a clone is in flight, or this old-UI teardown would kill
- // the clone's daemon and strand its lock/keep-alive service.
- boolean cloneActive = org.iiab.controller.sync.presentation.SyncProgressRepository.get().isActive()
- || org.iiab.controller.redesign.CloneSendSession.isActive();
- if (transport != null && !cloneActive) transport.stop();
- shareController.stopApkServerQuietly();
- if (!cloneActive) syncVm.releaseNetwork(); // ADFA-4496: drop the network binding when the receive is torn down
- disableSystemProtection(); // S8: ensure the watchdog stops if a transfer was cut short
- }
-
- // WATCHDOG PROTECTION UTILS
- @Override
- public void enableSystemProtection() {
- Context ctx = getContext();
- if (ctx == null) return; // S8: detached -> nothing to protect
- Intent intent = new Intent(ctx, WatchdogService.class);
- intent.setAction(WatchdogService.ACTION_START);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- ctx.startForegroundService(intent);
- } else {
- ctx.startService(intent);
- }
- }
-
- @Override
- public void disableSystemProtection() {
- Context ctx = getContext();
- if (ctx == null) return; // S8: detached; onDestroyView already handled teardown
- Intent intent = new Intent(ctx, WatchdogService.class);
- intent.setAction(WatchdogService.ACTION_STOP);
- ctx.startService(intent);
- }
-
- // SYSTEM RESTRICTION ENFORCER (PPK & CHILD PROCESSES)
- /** ADFA-4496: "optimized" now means the phantom-process monitor is NOT active (live check). */
- @Override
- public boolean isSystemOptimizedForSync() {
- return !org.iiab.controller.sync.transport.PhantomProcessHelper.isMonitoringLikelyActive(getContext());
- }
-
- /** ADFA-4496: informed pre-flight dialog — offers the version-appropriate remedy plus
- * "continue anyway" (the reactive safety net catches an actual kill). */
- @Override
- public void showPhantomWarningDialog(Runnable onContinue) {
- if (getContext() == null) return;
- BrandDialog b = new BrandDialog(requireContext())
- .setTitle(getString(R.string.phantom_warn_title))
- .setMessage(getString(R.string.phantom_warn_body));
- if (android.os.Build.VERSION.SDK_INT >= 34) {
- b.setPositive(getString(R.string.phantom_warn_open_dev), () ->
- org.iiab.controller.sync.transport.PhantomProcessHelper.openDeveloperOptions(requireContext()));
- } else {
- b.setPositive(getString(R.string.adb_enforcer_btn_setup), () -> {
- requireContext().getSharedPreferences(ADB_PREFS, Context.MODE_PRIVATE)
- .edit().putBoolean(PREF_FOCUS_ADB, true).apply();
- MainActivity mainAct = (MainActivity) getActivity();
- if (mainAct != null) {
- androidx.viewpager2.widget.ViewPager2 pager = mainAct.findViewById(R.id.view_pager);
- if (pager != null) pager.setCurrentItem(2, true);
- }
- });
- }
- b.setNeutral(getString(R.string.phantom_warn_continue), () -> {
- if (onContinue != null) onContinue.run();
- });
- b.setNegative(getString(R.string.cancel), null);
- b.show();
- }
-
- // --- ArchCheckHost (ADFA-4506) -----------------------------------------
- @Override
- public boolean isServerRunning() {
- return shareController.isServerRunning();
- }
-
- @Override
- public boolean isShareMode() {
- return rgSyncMode.getCheckedRadioButtonId() == R.id.rb_mode_share;
- }
- // --- ShareHost (ADFA-4506) ---------------------------------------------
- @Override
- public boolean isServerAlive() {
- return ServerStateRepository.get().current().alive;
- }
-
- @Override
- public void updateArchLabelsVisibility() {
- archCheckController.updateArchLabelsVisibility();
- }
-
- @Override
- public int getArchBits() {
- return archCheckController.getArchBits();
- }
- // --- ReceiveHost (ADFA-4506) -------------------------------------------
- @Override
- public void showArchIncompatibilityDialog(String message) {
- archCheckController.showArchIncompatibilityDialog(message);
- }
-
- @Override
- public void showArchCompatibilitySuccess(Runnable onComplete) {
- archCheckController.showArchCompatibilitySuccess(onComplete);
- }
-
- @Override
- public void launchQrScanner() {
- ScanOptions options = new ScanOptions();
- options.setDesiredBarcodeFormats(ScanOptions.QR_CODE);
- options.setPrompt(getString(R.string.sync_scanner_prompt));
- options.setCameraId(0);
- options.setBeepEnabled(false);
- options.setBarcodeImageEnabled(false);
- barcodeLauncher.launch(options);
- }
-
- @Override
- public void selectReceiveMode() {
- if (rgSyncMode.getCheckedRadioButtonId() != R.id.rb_mode_receive) {
- rgSyncMode.check(R.id.rb_mode_receive);
- }
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/UsageFragment.java b/controller/app/src/main/java/org/iiab/controller/UsageFragment.java
deleted file mode 100644
index 0bf9fb87f..000000000
--- a/controller/app/src/main/java/org/iiab/controller/UsageFragment.java
+++ /dev/null
@@ -1,603 +0,0 @@
-/*
- * ============================================================================
- * Name : UsageFragment.java
- * Author : IIAB Project
- * Copyright : Copyright (c) 2026 IIAB Project
- * Description : Usage Fragment Activity
- * ============================================================================
- */
-package org.iiab.controller;
-
-import android.content.Context;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.iiab.controller.ui.dialog.BrandDialog;
-import androidx.core.content.ContextCompat;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import android.Manifest;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import org.iiab.controller.hotspot.HotspotAvailability;
-import org.iiab.controller.hotspot.LocalHotspotManager;
-
-import org.iiab.controller.network.presentation.DnsSettingsUiState;
-import org.iiab.controller.network.presentation.DnsSettingsViewModel;
-import org.iiab.controller.network.presentation.DnsSettingsViewModelFactory;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-public class UsageFragment extends Fragment implements View.OnClickListener {
-
- private MainActivity mainActivity;
- // INTERFACE VARS
- private TextView logLabel, logWarning, logSizeText;
- private ServerLogView connectionLog;
- // ADFA-4640: observe the app-scoped log so the console survives tab switches / hide-show.
- private final LogRepository.Listener logListener = new LogRepository.Listener() {
- @Override public void onAppend(String line) { if (connectionLog != null) connectionLog.append(line); }
- @Override public void onCleared() { if (connectionLog != null) connectionLog.clear(); }
- };
- private Button button_browse_content, btnClearLog, btnCopyLog;
- private LinearLayout logActions, deckContainer;
- private ProgressBar logProgress;
- private ProgressButton btnServerControl;
-
- private DashboardManager dashboardManager;
-
- // Setup DNS (network slice, PR B)
- private CheckBox setup_dns_check;
- private LinearLayout dns_setup_fields;
- private EditText dns_primary, dns_secondary;
- private Button dns_accept;
- private TextView dns_result;
- private TextView dns_settings_label;
- private LinearLayout dns_settings_section;
- private DnsSettingsViewModel dnsViewModel;
- private boolean suppressDnsToggle = false;
-
- // ADFA-4520: LocalOnlyHotspot (LOHS) fallback, lives inside the Advanced settings section.
- private LinearLayout lohs_block;
- private CheckBox lohs_toggle;
- private TextView lohs_status, lohs_hint;
- private Button lohs_show_qr;
- private boolean suppressLohsToggle = false;
- private String lohsSsid = null, lohsPass = null;
- private final ActivityResultLauncher lohsLocationPerm =
- registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
- if (granted) {
- startLohs();
- } else {
- setLohsToggleChecked(false);
- if (getContext() != null) Toast.makeText(getContext(), R.string.lohs_need_location, Toast.LENGTH_LONG).show();
- }
- });
-
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
- if (context instanceof MainActivity) {
- mainActivity = (MainActivity) context;
- mainActivity.setUsageFragment(this);
- }
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_usage, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- org.iiab.controller.help.TooltipWiring.wireAll(view);
-
- // UI Bindings
- setup_dns_check = view.findViewById(R.id.setup_dns_check);
- dns_setup_fields = view.findViewById(R.id.dns_setup_fields);
- dns_primary = view.findViewById(R.id.dns_primary);
- dns_secondary = view.findViewById(R.id.dns_secondary);
- dns_accept = view.findViewById(R.id.dns_accept);
- dns_result = view.findViewById(R.id.dns_result);
- dns_settings_label = view.findViewById(R.id.dns_settings_label);
- dns_settings_section = view.findViewById(R.id.dns_settings_section);
- dns_settings_label.setText(String.format(getString(R.string.label_separator_up), getString(R.string.network_advanced_label)));
- dns_settings_label.setOnClickListener(v -> toggleVisibility(dns_settings_section, dns_settings_label, getString(R.string.network_advanced_label)));
- dnsViewModel = new ViewModelProvider(this, new DnsSettingsViewModelFactory(requireContext()))
- .get(DnsSettingsViewModel.class);
- dnsViewModel.state().observe(getViewLifecycleOwner(), this::renderDnsState);
- setup_dns_check.setOnCheckedChangeListener((btn, checked) -> {
- if (suppressDnsToggle) return;
- dnsViewModel.onSetupToggled(checked);
- });
- dns_accept.setOnClickListener(v -> dnsViewModel.onAccept(
- dns_primary.getText().toString(), dns_secondary.getText().toString()));
-
- // ADFA-4520: LocalOnlyHotspot section (inside Advanced settings), gated to API 26+.
- lohs_block = view.findViewById(R.id.lohs_block);
- lohs_toggle = view.findViewById(R.id.lohs_toggle);
- lohs_status = view.findViewById(R.id.lohs_status);
- lohs_hint = view.findViewById(R.id.lohs_hint);
- lohs_show_qr = view.findViewById(R.id.lohs_show_qr);
- if (!LocalHotspotManager.isSupported()) {
- lohs_block.setVisibility(View.GONE);
- } else {
- lohs_toggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (suppressLohsToggle) return;
- if (isChecked) requestLohsStart(); else LocalHotspotManager.get().stop();
- });
- lohs_show_qr.setOnClickListener(v -> {
- if (lohsSsid != null) {
- Intent qr = new Intent(getContext(), QrActivity.class);
- qr.putExtra(QrActivity.EXTRA_WIFI_SSID, lohsSsid);
- qr.putExtra(QrActivity.EXTRA_WIFI_PASS, lohsPass);
- startActivity(qr);
- }
- });
- LocalHotspotManager.get().state().observe(getViewLifecycleOwner(), this::renderLohs);
- }
- button_browse_content = view.findViewById(R.id.btnBrowseContent);
-
- logActions = view.findViewById(R.id.log_actions);
- btnClearLog = view.findViewById(R.id.btn_clear_log);
- btnCopyLog = view.findViewById(R.id.btn_copy_log);
- connectionLog = view.findViewById(R.id.connection_log);
- logProgress = view.findViewById(R.id.log_progress);
- logWarning = view.findViewById(R.id.log_warning_text);
- logSizeText = view.findViewById(R.id.log_size_text);
- logLabel = view.findViewById(R.id.log_label);
-
- deckContainer = view.findViewById(R.id.deck_container);
- btnServerControl = view.findViewById(R.id.btn_server_control);
-
- dashboardManager = new DashboardManager(requireActivity(), view);
-
- // Listeners
- button_browse_content.setOnClickListener(v -> mainActivity.handleBrowseContentClick(v));
- btnClearLog.setOnClickListener(this);
- btnCopyLog.setOnClickListener(this);
- logLabel.setOnClickListener(v -> handleLogToggle());
-
- btnServerControl.setOnClickListener(v -> {
- // --- Intercept based on State Machine ---
- SystemState state = ServerStateRepository.get().current().systemState;
- boolean isFullyInstalled = (state == SystemState.ONLINE || state == SystemState.OFFLINE);
-
- if (!isFullyInstalled) {
- Snackbar.make(v, R.string.server_not_installed_warning, 6000).show();
- return; // Stop execution here
- }
- // --------------------------------------------------
-
- // ADFA-4621: never toggle the server while a rootfs/module install is in flight —
- // concurrent proot sessions over the same rootfs corrupt the install.
- if (org.iiab.controller.install.presentation.InstallProgressRepository.get().isRunning()
- || org.iiab.controller.install.presentation.ModuleQueueRepository.get().isRunning()) {
- Snackbar.make(v, R.string.server_busy_install_lock, 6000).show();
- return;
- }
-
- if (mainActivity.targetServerState != null) return;
-
- mainActivity.serverTransitionText = !ServerStateRepository.get().current().alive ? getString(R.string.server_booting) : getString(R.string.server_shutting_down);
- mainActivity.targetServerState = !ServerStateRepository.get().current().alive;
-
- updateUIColorsAndVisibility();
- btnServerControl.startProgress();
-
- mainActivity.handleServerLaunchClick(v);
- });
-
- logLabel.setText(String.format(getString(R.string.label_separator_up), getString(R.string.connection_log_label)));
-
- updateUI();
- }
-
- @Override
- public void onClick(View v) {
- if (v.getId() == R.id.btn_clear_log) {
- showResetLogConfirmation();
- } else if (v.getId() == R.id.btn_copy_log) {
- ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getContent());
- if (clipboard != null) {
- clipboard.setPrimaryClip(clip);
- Toast.makeText(requireContext(), R.string.log_copied_toast, Toast.LENGTH_SHORT).show();
- }
- }
- }
-
- public void updateUI() {
- // Tunnel/VPN settings UI removed (ADFA-4553); no dynamic state to refresh here.
- }
-
- public void updateUIColorsAndVisibility() {
- if (!isAdded() || getContext() == null) {
- return;
- }
- if (button_browse_content == null) return;
-
- // Explore Button
- button_browse_content.setVisibility(View.VISIBLE);
- if (!ServerStateRepository.get().current().alive) {
- button_browse_content.setEnabled(true);
- button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_disabled));
- button_browse_content.setAlpha(1.0f);
- button_browse_content.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_on_accent));
- } else if (mainActivity.isNegotiating) {
- button_browse_content.setEnabled(true);
- button_browse_content.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_on_accent));
- } else {
- button_browse_content.setEnabled(true);
- button_browse_content.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_on_accent));
- button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_ready));
- button_browse_content.setAlpha(1.0f);
- }
-
- // Server Control Logic
- SystemState state = ServerStateRepository.get().current().systemState;
- boolean isFullyInstalled = (state == SystemState.ONLINE || state == SystemState.OFFLINE);
-
- if (!isFullyInstalled) {
- // SYSTEM NOT READY: Gray out the button
- btnServerControl.setAlpha(0.6f);
- btnServerControl.setText(R.string.launch_server);
- btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_disabled));
- } else if (mainActivity.targetServerState != null) {
- // TRANSITIONING STATE
- btnServerControl.setAlpha(0.6f);
- btnServerControl.setText(mainActivity.serverTransitionText);
- btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_disabled));
- } else {
- // SYSTEM READY: Normal behavior
- btnServerControl.setAlpha(1.0f);
- if (ServerStateRepository.get().current().alive) {
- btnServerControl.setText(R.string.stop_server);
- btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_danger));
- } else {
- btnServerControl.setText(R.string.launch_server);
- btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_success));
- }
- }
- }
-
- public void stopBtnProgress() {
- btnServerControl.stopProgress();
- }
-
- // =========================================================================
- // Empty methods kept to prevent crashes from MainActivity's legacy broadcast receivers
- // =========================================================================
- // ADFA-4520 helpers -------------------------------------------------------
-
- private void setLohsToggleChecked(boolean checked) {
- suppressLohsToggle = true;
- if (lohs_toggle != null) lohs_toggle.setChecked(checked);
- suppressLohsToggle = false;
- }
-
- private void requestLohsStart() {
- Context ctx = getContext();
- if (ctx == null) return;
- if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- lohsLocationPerm.launch(Manifest.permission.ACCESS_FINE_LOCATION);
- return;
- }
- startLohs();
- }
-
- private void startLohs() {
- Context ctx = getContext();
- if (ctx == null) return;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- LocalHotspotManager.get().start(ctx.getApplicationContext());
- }
- }
-
- private void renderLohs(LocalHotspotManager.State st) {
- if (lohs_status == null || st == null) return;
- switch (st.phase) {
- case STARTING:
- lohs_status.setVisibility(View.VISIBLE);
- lohs_status.setText(R.string.lohs_status_starting);
- lohs_show_qr.setVisibility(View.GONE);
- break;
- case ON:
- lohsSsid = st.ssid;
- lohsPass = st.passphrase;
- setLohsToggleChecked(true);
- lohs_status.setVisibility(View.VISIBLE);
- lohs_status.setText(getString(R.string.lohs_status_on, st.ssid == null ? "" : st.ssid));
- lohs_show_qr.setVisibility(View.VISIBLE);
- break;
- case FAILED:
- setLohsToggleChecked(false);
- lohs_status.setVisibility(View.VISIBLE);
- lohs_status.setText(getString(R.string.lohs_status_failed, st.failureReason));
- lohs_show_qr.setVisibility(View.GONE);
- break;
- case OFF:
- default:
- setLohsToggleChecked(false);
- lohsSsid = null;
- lohsPass = null;
- lohs_status.setVisibility(View.GONE);
- lohs_show_qr.setVisibility(View.GONE);
- break;
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- // ADFA-4640: attach to the app-scoped log and render the current buffer.
- LogRepository.get().addListener(logListener);
- if (connectionLog != null) {
- connectionLog.setContent(android.text.TextUtils.join("\n", LogRepository.get().snapshot()));
- }
- maybeRecommendLohs();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- LogRepository.get().removeListener(logListener);
- }
-
- /**
- * ADFA-4520: recommend LOHS only when BOTH conditions hold (AND, not OR): the operator
- * tried the native hotspot and it did not come up, AND there is no SIM in the device.
- * Reveals + pulses the Advanced settings header (where LOHS lives) and shows a snackbar.
- */
- private void maybeRecommendLohs() {
- Context ctx = getContext();
- if (ctx == null || !LocalHotspotManager.isSupported()) return;
- LocalHotspotManager mgr = LocalHotspotManager.get();
- boolean triedNative = mgr.wasNativeHotspotAttempted();
- boolean hotspotUp = mainActivity != null && mainActivity.isHotspotActive();
- boolean simAbsent = HotspotAvailability.isSimAbsent(ctx);
- if (!(triedNative && !hotspotUp && simAbsent)) return;
- mgr.clearNativeHotspotAttempted();
- expandAdvancedSettings();
- pulseView(dns_settings_label);
- View anchor = null;
- if (mainActivity != null) {
- anchor = mainActivity.findViewById(R.id.main_coordinator);
- if (anchor == null) anchor = mainActivity.findViewById(android.R.id.content);
- }
- if (anchor != null) {
- Snackbars.make(anchor, R.string.lohs_recommend)
- .setAction(R.string.lohs_recommend_action, v -> {
- expandAdvancedSettings();
- pulseView(dns_settings_label);
- })
- .show();
- }
- }
-
- private void expandAdvancedSettings() {
- if (dns_settings_section != null && dns_settings_section.getVisibility() != View.VISIBLE) {
- toggleVisibility(dns_settings_section, dns_settings_label, getString(R.string.network_advanced_label));
- }
- }
-
- /**
- * ADFA-4520 recommendation cue. Mirrors DeployFragment#focusAdvancedMonitoring: blink the
- * header text colour (danger <-> normal, 400ms x5, reverse), resetting to normal at the end.
- */
- private void pulseView(View v) {
- if (!(v instanceof TextView) || getContext() == null) return;
- final TextView t = (TextView) v;
- final int normal = t.getCurrentTextColor();
- int danger = ContextCompat.getColor(requireContext(), R.color.status_danger);
- android.animation.ObjectAnimator anim = android.animation.ObjectAnimator.ofObject(
- t, "textColor", new android.animation.ArgbEvaluator(), normal, danger);
- anim.setDuration(400);
- anim.setRepeatCount(5);
- anim.setRepeatMode(android.animation.ValueAnimator.REVERSE);
- anim.addListener(new android.animation.AnimatorListenerAdapter() {
- @Override public void onAnimationEnd(android.animation.Animator a) { t.setTextColor(normal); }
- });
- anim.start();
- }
-
- public void startFusionPulse() {
- }
-
- public void startExitPulse() {
- }
-
- public void finalizeEntryPulse() {
- }
-
- public void finalizeExitPulse() {
- }
-
- public void addToLog(String message) {
- // ADFA-4640: funnel into the app-scoped source of truth; the console (which
- // observes LogRepository) renders it. Timestamping is centralized in the repo.
- LogRepository.get().append(message);
- }
-
- public void updateLogSizeUI() {
- // 3. We added the lock so that it does not call the Context if it is in another tab
- if (!isAdded() || getContext() == null || logSizeText == null) return;
-
- // 4. We use getContext() instead of requireContext()
- // ADFA-4640: report the console's own persistent log (server_log.txt), not the watchdog blackbox.
- String sizeStr = LogManager.formatSize(getContext(), LogRepository.get().fileSizeBytes());
- logSizeText.setText(getString(R.string.log_size_format, sizeStr));
- }
-
- public void updateConnectivityLeds(boolean wifiOn, boolean hotspotOn) {
- if (dashboardManager != null) {
- dashboardManager.updateConnectivityLeds(wifiOn, hotspotOn);
- }
- }
-
- public boolean isLogVisible() {
- return connectionLog != null && connectionLog.getVisibility() == View.VISIBLE;
- }
-
- private void handleLogToggle() {
- boolean isOpening = connectionLog.getVisibility() == View.GONE;
- if (isOpening) {
- if (mainActivity.isReadingLogs) return;
- mainActivity.isReadingLogs = true;
- if (connectionLog != null) {
- connectionLog.setContent(android.text.TextUtils.join("\n", LogRepository.get().snapshot()));
- }
- if (logProgress != null) logProgress.setVisibility(View.VISIBLE);
-
- // ADFA-4640: read the blackbox only for the rapid-growth warning + size; do NOT
- // setContent from it (that used to wipe the live install/Ansible log on reopen).
- LogManager.readLogsAsync(requireContext(), (logContent, isRapidGrowth) -> {
- if (logProgress != null) logProgress.setVisibility(View.GONE);
- if (logWarning != null)
- logWarning.setVisibility(isRapidGrowth ? View.VISIBLE : View.GONE);
- updateLogSizeUI();
- mainActivity.isReadingLogs = false;
- });
- mainActivity.startLogSizeUpdates();
- } else {
- mainActivity.stopLogSizeUpdates();
- }
- toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label));
- logActions.setVisibility(connectionLog.getVisibility());
- if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility());
- }
-
- private void toggleVisibility(View view, TextView label, String text) {
- boolean isGone = view.getVisibility() == View.GONE;
- view.setVisibility(isGone ? View.VISIBLE : View.GONE);
- label.setText(String.format(getString(isGone ? R.string.label_separator_down : R.string.label_separator_up), text));
- }
-
- private void showResetLogConfirmation() {
- new BrandDialog(requireContext())
- .setTitle(R.string.log_reset_confirm_title)
- .setMessage(R.string.log_reset_confirm_msg)
- .setDestructive(R.string.reset_log, () -> {
- LogManager.clearLogs(requireContext(), new LogManager.LogClearCallback() {
- @Override
- public void onSuccess() {
- LogRepository.get().clear();
- addToLog(getString(R.string.log_reset_user));
- if (logWarning != null) logWarning.setVisibility(View.GONE);
- updateLogSizeUI();
- Toast.makeText(requireContext(), R.string.log_cleared_toast, Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public void onError(String message) {
- Toast.makeText(requireContext(), getString(R.string.failed_reset_log, message), Toast.LENGTH_SHORT).show();
- }
- });
- })
- .setNegative(R.string.cancel, null).show();
- }
-
- public void savePrefsFromUI() {
- // Tunnel/VPN prefs removed (ADFA-4553); nothing to persist from this screen.
- }
-
- private void renderDnsState(DnsSettingsUiState st) {
- if (setup_dns_check == null) return;
- suppressDnsToggle = true;
- setup_dns_check.setChecked(st.customEnabled);
- suppressDnsToggle = false;
- dns_setup_fields.setVisibility(st.customEnabled ? View.VISIBLE : View.GONE);
- if (st.status == DnsSettingsUiState.Status.IDLE || st.status == DnsSettingsUiState.Status.UNREACHABLE) {
- dns_primary.setText(st.primary);
- dns_secondary.setText(st.secondary);
- }
- switch (st.status) {
- case TESTING:
- dns_result.setVisibility(View.VISIBLE);
- dns_result.setText(getString(R.string.dns_status_testing));
- dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary));
- break;
- case APPLIED:
- dns_result.setVisibility(View.VISIBLE);
- dns_result.setText(getString(R.string.dns_status_ok));
- dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_success));
- break;
- case INVALID:
- case UNREACHABLE:
- dns_result.setVisibility(View.VISIBLE);
- dns_result.setText(st.message != null ? st.message : "");
- dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_warning));
- break;
- default:
- dns_result.setVisibility(View.GONE);
- break;
- }
- }
-
- public void highlightServerButton() {
- if (deckContainer == null || !isAdded()) return;
-
- requireActivity().runOnUiThread(() -> {
- // We save the original padding (the 3dp)
- int pL = deckContainer.getPaddingLeft();
- int pT = deckContainer.getPaddingTop();
- int pR = deckContainer.getPaddingRight();
- int pB = deckContainer.getPaddingBottom();
-
- // We use ofArgb for a perfect color transition
- android.animation.ValueAnimator colorAnim = android.animation.ValueAnimator.ofArgb(
- Color.TRANSPARENT,
- ContextCompat.getColor(requireContext(), R.color.status_info) // Color Cyan
- );
- colorAnim.setDuration(350);
- colorAnim.setRepeatCount(5);
- colorAnim.setRepeatMode(android.animation.ValueAnimator.REVERSE);
-
- float cornerRadius = getResources().getDisplayMetrics().density * 10; // ~10dp
-
- colorAnim.addUpdateListener(animator -> {
- int color = (int) animator.getAnimatedValue();
- android.graphics.drawable.GradientDrawable gd = new android.graphics.drawable.GradientDrawable();
- gd.setColor(color);
- gd.setCornerRadius(cornerRadius);
- deckContainer.setBackground(gd);
- deckContainer.setPadding(pL, pT, pR, pB);
- });
-
- colorAnim.addListener(new android.animation.AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(android.animation.Animator animation) {
- deckContainer.setBackgroundColor(Color.TRANSPARENT);
- deckContainer.setPadding(pL, pT, pR, pB);
- }
- });
-
- colorAnim.start();
- });
- }
-}
\ No newline at end of file
diff --git a/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupController.java b/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupController.java
deleted file mode 100644
index d7a001ec5..000000000
--- a/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupController.java
+++ /dev/null
@@ -1,714 +0,0 @@
-/*
- * ============================================================================
- * Name : BackupController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Backup/restore presentation logic carved out of DeployFragment
- * (strangler-fig). Owns the backup selection state + SAF launchers
- * and wires the backup/restore/import buttons + the backup menu.
- * Being a non-Fragment class, it removes this large, deeply-nested
- * call graph from the Fragment that the androidx
- * UnsafeFragmentLifecycleObserverDetector walks (the lint hang;
- * see controller/docs/TECH_DEBT_PLAN.md). No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.backup.presentation;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.net.Uri;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.RadioButton;
-import android.widget.TextView;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.content.ContextCompat;
-import androidx.core.widget.NestedScrollView;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.MainActivity;
-import org.iiab.controller.ProgressButton;
-import org.iiab.controller.R;
-import org.iiab.controller.TarExtractor;
-import org.iiab.controller.util.ProcessRunner;
-
-import java.io.File;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.List;
-import org.iiab.controller.ui.dialog.BrandDialog;
-
-public final class BackupController {
-
- private static final String TAG = "IIAB-BackupController";
- private static final String[] IMPORT_SPINNER = {"\u28BF", "\u28FB", "\u28FD", "\u28FE", "\u28F7", "\u28EF", "\u28DF", "\u287F"};
-
- private final Fragment fragment;
- private final BackupHost host;
-
- // Set in bind() (cross-feature views are borrowed; the Fragment also touches some).
- private MainActivity mainAct;
- private File backupsDir;
- private File iiabRootDir;
- private Button btnImportBackup;
- private ProgressButton btnAdvancedBackup;
- private ProgressButton btnAdvancedRestore;
- private TextView txtSelectBackupTitle;
- private TextView txtBackupStatus;
- private LinearLayout containerBackupList;
- private LinearLayout restoreLogPanel;
- private TextView restoreLogText;
- private TextView restoreLogResult;
- private NestedScrollView restoreLogScroll;
-
- // Owned state (only the backup/restore feature touches these).
- private String selectedBackupFile = null;
- private ActivityResultLauncher importBackupLauncher;
- private ActivityResultLauncher exportBackupLauncher;
- private android.os.Handler importSpinnerHandler;
- private int importSpinnerFrame = 0;
-
- public BackupController(Fragment fragment, BackupHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Wire the backup/restore/import controls. Call from onViewCreated. */
- public void bind(MainActivity mainAct, File backupsDir, File iiabRootDir,
- Button btnImportBackup, ProgressButton btnAdvancedBackup, ProgressButton btnAdvancedRestore,
- TextView txtSelectBackupTitle, TextView txtBackupStatus, LinearLayout containerBackupList,
- LinearLayout restoreLogPanel, TextView restoreLogText, TextView restoreLogResult,
- NestedScrollView restoreLogScroll) {
- this.mainAct = mainAct;
- this.backupsDir = backupsDir;
- this.iiabRootDir = iiabRootDir;
- this.btnImportBackup = btnImportBackup;
- this.btnAdvancedBackup = btnAdvancedBackup;
- this.btnAdvancedRestore = btnAdvancedRestore;
- this.txtSelectBackupTitle = txtSelectBackupTitle;
- this.txtBackupStatus = txtBackupStatus;
- this.containerBackupList = containerBackupList;
- this.restoreLogPanel = restoreLogPanel;
- this.restoreLogText = restoreLogText;
- this.restoreLogResult = restoreLogResult;
- this.restoreLogScroll = restoreLogScroll;
- bindBackupButtonLogic();
- bindBackupMenuLogic();
- refreshRestoreButtonLogic();
- }
-
- public void registerLaunchers() {
- importBackupLauncher = fragment.registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> {
- if (uri != null) importBackupSafely(uri);
- });
-
- exportBackupLauncher = fragment.registerForActivityResult(new ActivityResultContracts.CreateDocument("application/gzip"), uri -> {
- if (uri != null && selectedBackupFile != null)
- exportBackupSafely(uri, selectedBackupFile);
- });
- }
-
- private void bindBackupButtonLogic() {
- if (btnAdvancedBackup == null) return;
- btnAdvancedBackup.setOnClickListener(v -> {
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
- if (host.isSystemBusy() && !host.isBackupInProgress()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
- if (host.isBackupInProgress()) {
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.install_msg_backup_in_progress_title))
- .setMessage(fragment.getString(R.string.install_msg_backup_in_progress_body))
- .setDestructive(fragment.getString(R.string.install_btn_force_stop_process), () -> {
- host.setBackupInProgress(false);
- btnAdvancedBackup.setText(fragment.getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_backup_aborted)).show();
- })
- .setNegative(fragment.getString(R.string.install_btn_let_finish), null)
- .show();
- return;
- }
-
- host.setBackupInProgress(true);
- btnAdvancedBackup.setText(fragment.getString(R.string.install_msg_compressing));
- btnAdvancedBackup.startProgress();
- Snackbars.make(v, fragment.getString(R.string.install_msg_creating_backup)).show();
-
- new Thread(() -> {
- host.enableSystemProtection();
- try {
- // Format: iiab-oa_rootfs_$year.$day_of_year_3_digits_$id_$arch.tar.gz
- java.util.Calendar calendar = java.util.Calendar.getInstance();
- int year = calendar.get(java.util.Calendar.YEAR);
- int dayOfYear = calendar.get(java.util.Calendar.DAY_OF_YEAR);
- String arch = host.getTermuxArch();
-
- // --- AUTO-INCREMENTAL ID LOGIC ---
- android.content.SharedPreferences prefs = fragment.requireContext().getSharedPreferences(fragment.getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
-
- // We check if we continue on the same day. If it is a new day, we reset the ID to 1
- int lastSavedDay = prefs.getInt("backup_last_day", -1);
- int currentId;
-
- if (lastSavedDay == dayOfYear) {
- // Same day, we increase the ID
- currentId = prefs.getInt("backup_daily_id", 0) + 1;
- } else {
- // New day, we start from 1
- currentId = 1;
- prefs.edit().putInt("backup_last_day", dayOfYear).apply();
- }
-
- // We save the new ID in preferences for next time
- prefs.edit().putInt("backup_daily_id", currentId).apply();
-
- // We construct the final name with the ID
- String fileName = String.format(java.util.Locale.US, "iiab-oa_%04d.%03d_%d_%s.tar.gz", year, dayOfYear, currentId, arch);
- File backupFile = new File(backupsDir, fileName);
-
- File staticTar = new File(fragment.requireContext().getApplicationInfo().nativeLibraryDir, "libtar.so");
- File staticGzip = new File(fragment.requireContext().getApplicationInfo().nativeLibraryDir, "libgzip.so");
- String tarBin = staticTar.exists() ? staticTar.getAbsolutePath() : "tar";
- String gzipBin = staticGzip.exists() ? staticGzip.getAbsolutePath() : "gzip";
-
- // Stamp an identity manifest into the backup so a re-import is
- // recognized (kind/arch) AND explicitly declares it carries NO
- // integrity checksum (origin=device-backup) — we do NOT turn the
- // phone into a builder. It is staged in a temp tree and packed
- // FIRST (a second `-C`) so RootfsArchiveValidator reads it from
- // the first tar header without decompressing the whole archive.
- // See docs/ROOTFS_MANIFEST.md.
- String manifestArg = null;
- File mfStageRoot = new File(fragment.requireContext().getCacheDir(), "mfstage");
- try {
- if (mfStageRoot.exists()) {
- ProcessRunner.run(new String[]{"rm", "-rf", mfStageRoot.getAbsolutePath()});
- }
- File iiabStage = new File(mfStageRoot, "installed-rootfs/iiab");
- if (iiabStage.mkdirs()) {
- String appAbi = org.iiab.controller.deploy.data.RootfsManifest.appAbiId();
- String debArch = appAbi.contains("64") ? "arm64" : "armhf";
- String built = String.format(java.util.Locale.US, "%04d.%03d", year, dayOfYear);
- String identityJson = "{\"schema\":1,\"kind\":\"iiab-rootfs\",\"arch\":\""
- + appAbi + "\",\"deb_arch\":\"" + debArch + "\",\"built\":\""
- + built + "\",\"builder\":\"knowledgetogo-app\",\"origin\":\"device-backup\"}";
- java.io.FileOutputStream mfo =
- new java.io.FileOutputStream(new File(iiabStage, ".iiab-rootfs.json"));
- mfo.write(identityJson.getBytes("UTF-8"));
- mfo.close();
- manifestArg = "-C '" + mfStageRoot.getAbsolutePath()
- + "' 'installed-rootfs/iiab/.iiab-rootfs.json' ";
- }
- } catch (Exception mfe) {
- Log.w(TAG, "Could not stage identity manifest for backup: " + mfe.getMessage());
- manifestArg = null;
- }
-
- // D11: single-quote the interpolated paths so the backup pipe is robust
- // even if a path ever contains spaces/metacharacters (app-internal today).
- String cmd = "'" + tarBin + "' -cf - "
- + (manifestArg != null ? manifestArg : "")
- + "-C '" + iiabRootDir.getAbsolutePath()
- + "' installed-rootfs | '" + gzipBin + "' > '" + backupFile.getAbsolutePath() + "'";
- // D12: ProcessRunner drains stderr so a large backup with tar warnings
- // cannot deadlock on a full pipe buffer.
- ProcessRunner.Result backupResult = ProcessRunner.run(new String[]{"/system/bin/sh", "-c", cmd});
- int exitCode = backupResult.exitCode;
- if (exitCode != 0) {
- Log.w(TAG, "Backup pipe failed (exit " + exitCode + "): " + backupResult.output);
- }
-
- mainAct.runOnUiThread(() -> {
- if (host.isBackupInProgress()) {
- if (exitCode == 0) {
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_backup_complete, backupFile.getName())).show();
- selectedBackupFile = backupFile.getName();
- } else {
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_backup_failed, exitCode)).show();
- if (backupFile.exists()) backupFile.delete();
-
- // If it fails, we revert the ID so as not to waste numbers
- prefs.edit().putInt("backup_daily_id", currentId - 1).apply();
- }
- } else {
- if (backupFile.exists()) backupFile.delete();
- prefs.edit().putInt("backup_daily_id", currentId - 1).apply();
- }
- host.setBackupInProgress(false);
- btnAdvancedBackup.setText(fragment.getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
- host.updateDynamicButtons();
- host.disableSystemProtection();
- });
- } catch (Exception e) {
- mainAct.runOnUiThread(() -> {
- host.setBackupInProgress(false);
- btnAdvancedBackup.setText(fragment.getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_backup_error, e.getMessage())).show();
- host.updateDynamicButtons();
- host.disableSystemProtection();
- });
- }
- }).start();
- });
-
- if (btnImportBackup != null) {
- // 1. We load the native icon
- android.graphics.drawable.Drawable importIcon = ContextCompat.getDrawable(fragment.requireContext(), android.R.drawable.stat_sys_download);
- if (importIcon != null) {
- importIcon.setTint(ContextCompat.getColor(fragment.requireContext(), R.color.status_success));
- btnImportBackup.setCompoundDrawablesWithIntrinsicBounds(importIcon, null, null, null);
- btnImportBackup.setCompoundDrawablePadding(24);
-
- // 2. We center the content internally
- btnImportBackup.setGravity(android.view.Gravity.CENTER);
- btnImportBackup.setPadding(0, 0, 0, 0);
-
- // 3. We change the width to wrap_content and center the button in its container
- if (btnImportBackup.getLayoutParams() instanceof LinearLayout.LayoutParams) {
- LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) btnImportBackup.getLayoutParams();
- params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
- params.gravity = android.view.Gravity.CENTER_HORIZONTAL;
- btnImportBackup.setLayoutParams(params);
- }
- }
-
- btnImportBackup.setOnClickListener(v -> {
- importBackupLauncher.launch(new String[]{"application/gzip", "application/x-gzip", "*/*"});
- });
- }
- }
-
- private void bindBackupMenuLogic() {
- if (txtSelectBackupTitle == null) return;
- txtSelectBackupTitle.setOnClickListener(v -> {
- boolean isCollapsed = containerBackupList.getVisibility() == View.GONE;
- if (isCollapsed) {
- containerBackupList.setVisibility(View.VISIBLE);
- txtSelectBackupTitle.setText(fragment.getString(R.string.install_adv_select_backup_open));
- containerBackupList.removeAllViews();
- selectedBackupFile = null;
-
- File[] backups = backupsDir.listFiles((dir, name) -> name.endsWith(".tar.gz") || name.endsWith(".tar.xz"));
- if (backups == null || backups.length == 0) {
- TextView noBackups = new TextView(fragment.requireContext());
- noBackups.setText(fragment.getString(R.string.install_msg_no_backups));
- noBackups.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger));
- containerBackupList.addView(noBackups);
- } else {
- java.util.Arrays.sort(backups, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
-
- LinearLayout listContainer = new LinearLayout(fragment.requireContext());
- listContainer.setOrientation(LinearLayout.VERTICAL);
-
- List radioButtons = new ArrayList<>();
- int iconPadding = (int) (12 * fragment.getResources().getDisplayMetrics().density);
-
- // Variable to alternate colors (Zebra Effect)
- boolean isEvenRow = true;
-
- for (File b : backups) {
- String filename = b.getName();
- String size = String.format(java.util.Locale.US, "%.2f MB", b.length() / (1024.0 * 1024.0));
- String date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.US).format(new java.util.Date(b.lastModified()));
-
- // MAIN ROW
- LinearLayout row = new LinearLayout(fragment.requireContext());
- row.setOrientation(LinearLayout.HORIZONTAL);
- row.setGravity(android.view.Gravity.CENTER_VERTICAL);
-
- // Apply subtle alternating background color
- if (isEvenRow) {
- row.setBackgroundColor(ContextCompat.getColor(fragment.requireContext(), R.color.surface_section)); // Slightly lighter
- } else {
- row.setBackgroundColor(Color.TRANSPARENT); // Normal dark
- }
- isEvenRow = !isEvenRow; // Alternar para la siguiente fila
-
- LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- rowParams.setMargins(0, 0, 0, 8); // Separation between cards
- row.setLayoutParams(rowParams);
- row.setPadding(8, 8, 8, 8);
-
- // RADIO BUTTON AND TEXT
- android.widget.RadioButton rb = new android.widget.RadioButton(fragment.requireContext());
- rb.setText(fragment.getString(R.string.install_msg_backup_details, filename, size, date));
- rb.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_primary));
- rb.setPadding(0, 8, 0, 8);
- rb.setTag(filename);
-
- LinearLayout.LayoutParams rbParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
- rb.setLayoutParams(rbParams);
- radioButtons.add(rb);
-
- // Selection logic (Applied to the ENTIRE row, not just the radio button)
- View.OnClickListener selectRowListener = rowView -> {
- for (android.widget.RadioButton other : radioButtons) {
- other.setChecked(other == rb);
- }
- selectedBackupFile = rb.isChecked() ? filename : null;
- refreshRestoreButtonLogic();
- };
-
- // We assign the click to both the RadioButton and the parent Layout
- rb.setOnClickListener(selectRowListener);
- row.setOnClickListener(selectRowListener);
-
- // EXPORT BUTTON
- android.widget.ImageButton btnExport = new android.widget.ImageButton(fragment.requireContext());
- btnExport.setImageResource(android.R.drawable.stat_sys_upload);
- btnExport.setBackgroundColor(Color.TRANSPARENT);
- btnExport.setColorFilter(ContextCompat.getColor(fragment.requireContext(), R.color.status_success));
- btnExport.setPadding(iconPadding, iconPadding, iconPadding, iconPadding);
- org.iiab.controller.help.ViewTooltips.attachLongPress(btnExport, org.iiab.controller.help.TooltipCategory.K2GO, org.iiab.controller.help.TooltipTag.DEPLOY_EXPORT_BACKUP);
-
- btnExport.setOnClickListener(btn -> {
- selectedBackupFile = filename;
- for (android.widget.RadioButton other : radioButtons) {
- other.setChecked(other == rb);
- }
- refreshRestoreButtonLogic();
- exportBackupLauncher.launch(selectedBackupFile);
- });
-
- // DELETE BUTTON
- android.widget.ImageButton btnDelete = new android.widget.ImageButton(fragment.requireContext());
- btnDelete.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
- btnDelete.setBackgroundColor(Color.TRANSPARENT);
- btnDelete.setColorFilter(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger));
- btnDelete.setPadding(iconPadding, iconPadding, iconPadding, iconPadding);
- org.iiab.controller.help.ViewTooltips.attachLongPress(btnDelete, org.iiab.controller.help.TooltipCategory.K2GO, org.iiab.controller.help.TooltipTag.DEPLOY_DELETE_BACKUP);
-
- btnDelete.setOnClickListener(btn -> {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.install_dialog_delete_backup_title)
- .setMessage(fragment.getString(R.string.install_dialog_delete_backup_msg, filename))
- .setDestructive(R.string.install_btn_delete_confirm, () -> {
- File toDelete = new File(backupsDir, filename);
- if (toDelete.delete()) {
- if (filename.equals(selectedBackupFile)) selectedBackupFile = null;
- txtSelectBackupTitle.performClick();
- txtSelectBackupTitle.performClick();
- Snackbars.make(fragment.getView(), R.string.install_msg_backup_deleted).show();
- }
- })
- .setNegative(R.string.cancel, null)
- .show();
- });
-
- row.addView(rb);
- row.addView(btnExport);
- row.addView(btnDelete);
-
- listContainer.addView(row);
- }
- containerBackupList.addView(listContainer);
- }
- refreshRestoreButtonLogic();
- } else {
- containerBackupList.setVisibility(View.GONE);
- txtSelectBackupTitle.setText(fragment.getString(R.string.install_adv_select_backup));
- }
- });
- }
-
- private void refreshRestoreButtonLogic() {
- MainActivity mainAct = (MainActivity) fragment.getActivity();
- if (mainAct == null || btnAdvancedRestore == null) return;
-
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- btnAdvancedRestore.setAlpha(0.5f);
- btnAdvancedRestore.setOnClickListener(v -> Snackbars.make(v, R.string.install_msg_server_running_lock).show());
- return;
- }
-
- if (selectedBackupFile == null) {
- btnAdvancedRestore.setAlpha(0.5f);
- btnAdvancedRestore.setOnClickListener(v -> Snackbars.make(v, R.string.install_msg_select_backup_first).show());
- } else {
- btnAdvancedRestore.setAlpha(1.0f);
- btnAdvancedRestore.setOnClickListener(v -> {
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
- if (host.isSystemBusy()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
-
- host.setRestoring(true);
- host.updateDynamicButtons();
- Snackbars.make(v, fragment.getString(R.string.install_msg_restore_starting, selectedBackupFile)).show();
- mainAct.invalidateModuleStateTrust();
-
- File backupFile = new File(new File(fragment.requireContext().getFilesDir(), "rootfs/backups"), selectedBackupFile);
- if (!backupFile.exists()) {
- host.setRestoring(false);
- host.updateDynamicButtons();
- Snackbars.make(v, R.string.install_error_backup_missing).show();
- return;
- }
-
- btnAdvancedRestore.setEnabled(false);
- btnAdvancedRestore.setText(fragment.getString(R.string.install_status_restoring));
- btnAdvancedRestore.startProgress();
- if (restoreLogPanel != null) {
- restoreLogPanel.setVisibility(View.VISIBLE);
- if (restoreLogText != null) {
- // Transparency at restore (the meaningful moment): note when the
- // backup carries no integrity checksum / no manifest.
- org.iiab.controller.deploy.data.RootfsManifest.Identity rid =
- org.iiab.controller.deploy.data.RootfsManifest.read(backupFile.getAbsolutePath());
- if (!rid.present) {
- restoreLogText.setText(fragment.getString(R.string.install_warn_manifest_missing) + "\n\n");
- } else if ("device-backup".equals(rid.origin)) {
- restoreLogText.setText(fragment.getString(R.string.install_warn_no_checksum) + "\n\n");
- } else {
- restoreLogText.setText("");
- }
- }
- if (restoreLogResult != null) restoreLogResult.setText("");
- }
- File iiabRootDir = new File(fragment.requireContext().getFilesDir(), "rootfs");
- TarExtractor tarExtractor = new TarExtractor();
-
- host.enableSystemProtection();
- tarExtractor.startExtraction(fragment.requireContext(), backupFile.getAbsolutePath(), iiabRootDir.getAbsolutePath(), true, new TarExtractor.ExtractionListener() {
- @Override
- public void onComplete(String destDir) {
- mainAct.runOnUiThread(() -> {
- host.setRestoring(false);
- host.disableSystemProtection();
- btnAdvancedRestore.setEnabled(true);
- btnAdvancedRestore.setText(fragment.getString(R.string.install_btn_restore));
- Snackbars.make(fragment.getView(), R.string.install_success_restore).show();
- if (restoreLogResult != null) { restoreLogResult.setText("\u2713"); restoreLogResult.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_success)); }
- btnAdvancedRestore.stopProgress();
- host.updateDynamicButtons();
- });
- }
-
- @Override
- public void onError(String error) {
- mainAct.runOnUiThread(() -> {
- host.setRestoring(false);
- host.disableSystemProtection();
- btnAdvancedRestore.setEnabled(true);
- btnAdvancedRestore.setText(fragment.getString(R.string.install_btn_restore));
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_restore_failed) + " " + error).show();
- if (restoreLogResult != null) { restoreLogResult.setText("\u2717"); restoreLogResult.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_warning)); }
- btnAdvancedRestore.stopProgress();
- host.updateDynamicButtons();
- });
- }
-
- @Override
- public void onProgress(String line) {
- if (restoreLogText == null) return;
- restoreLogText.append(line + "\n");
- if (restoreLogScroll != null) {
- restoreLogScroll.post(() -> restoreLogScroll.fullScroll(View.FOCUS_DOWN));
- }
- }
- });
- });
- }
- }
-
- private void startImportSpinner() {
- stopImportSpinner();
- importSpinnerFrame = 0;
- importSpinnerHandler = new android.os.Handler(android.os.Looper.getMainLooper());
- final Runnable r = new Runnable() {
- @Override public void run() {
- if (btnImportBackup != null) {
- String f = IMPORT_SPINNER[importSpinnerFrame++ % IMPORT_SPINNER.length];
- btnImportBackup.setText(fragment.getString(R.string.install_msg_importing) + " " + f);
- }
- if (importSpinnerHandler != null) importSpinnerHandler.postDelayed(this, 90);
- }
- };
- importSpinnerHandler.post(r);
- }
-
- private void stopImportSpinner() {
- if (importSpinnerHandler != null) {
- importSpinnerHandler.removeCallbacksAndMessages(null);
- importSpinnerHandler = null;
- }
- }
-
- /** Best-effort original filename from a SAF content:// URI (DISPLAY_NAME), or null. */
- private String queryDisplayName(Uri uri) {
- try (android.database.Cursor c = fragment.requireContext().getContentResolver()
- .query(uri, new String[]{android.provider.OpenableColumns.DISPLAY_NAME}, null, null, null)) {
- if (c != null && c.moveToFirst()) {
- int idx = c.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
- if (idx >= 0) return c.getString(idx);
- }
- } catch (Exception e) {
- Log.w(TAG, "queryDisplayName failed: " + e.getMessage());
- }
- return null;
- }
-
- /** Show a Snackbar whose visible time scales with the message length (reading time). */
- private void showImportSnackbar(CharSequence text) {
- View v = fragment.getView();
- if (v != null) {
- Snackbar.make(v, text,
- org.iiab.controller.util.SnackbarDuration.millisForText(text.toString())).show();
- }
- }
-
- private void importBackupSafely(Uri sourceUri) {
- host.setImporting(true);
- host.updateDynamicButtons();
- btnImportBackup.setEnabled(false);
- startImportSpinner();
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_importing)).show();
-
- new Thread(() -> {
- host.enableSystemProtection();
- try {
- File backupsDir = new File(fragment.requireContext().getFilesDir(), "rootfs/backups");
- if (!backupsDir.exists()) backupsDir.mkdirs();
-
- // Keep the imported file's EXACT name; disambiguate with -1/-2/... on collision.
- String desiredName = queryDisplayName(sourceUri);
- java.util.Set existingNames = new java.util.HashSet<>();
- File[] existingFiles = backupsDir.listFiles();
- if (existingFiles != null) {
- for (File f : existingFiles) existingNames.add(f.getName());
- }
- String fileName = org.iiab.controller.backup.domain.BackupNameResolver.resolve(desiredName, existingNames);
- File destFile = new File(backupsDir, fileName);
-
- InputStream is = fragment.requireContext().getContentResolver().openInputStream(sourceUri);
- OutputStream os = new java.io.FileOutputStream(destFile);
- byte[] buffer = new byte[8192];
- int length;
- while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
- }
- os.flush();
- os.close();
- is.close();
-
- // Gate the import: must be a valid rootfs of THIS app's architecture
- // (ABI policy). Reject and delete otherwise.
- org.iiab.controller.deploy.data.RootfsArchiveValidator.Result vr =
- org.iiab.controller.deploy.data.RootfsArchiveValidator
- .validate(fragment.requireContext(), destFile.getAbsolutePath());
- boolean okValidated =
- vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK;
- boolean okNoManifest =
- vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_MANIFEST;
- boolean okNoChecksum =
- vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_CHECKSUM;
- if (!okValidated && !okNoManifest && !okNoChecksum) {
- if (destFile.exists()) destFile.delete();
- final int errMsg;
- if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.WRONG_ARCH) {
- errMsg = R.string.install_error_wrong_arch;
- } else if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.CORRUPT) {
- errMsg = R.string.install_error_corrupt;
- } else {
- errMsg = R.string.install_error_not_rootfs;
- }
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> {
- host.setImporting(false);
- stopImportSpinner(); // stop the braille spinner; rejection ends the import
- host.updateDynamicButtons();
- btnImportBackup.setEnabled(true);
- btnImportBackup.setText(fragment.getString(R.string.install_btn_import_backup));
- showImportSnackbar(fragment.getString(errMsg));
- });
- }
- return;
- }
- // Soft phase: no identity manifest -> import is allowed, but warn the
- // user (a future version will validate silently). See docs/ROOTFS_MANIFEST.md.
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> {
- host.setImporting(false);
- stopImportSpinner();
- btnImportBackup.setEnabled(true);
- btnImportBackup.setText(fragment.getString(R.string.install_btn_import_backup));
- selectedBackupFile = fileName;
- host.updateDynamicButtons();
- // One snackbar only (Snackbar replaces, never queues): fold the
- // no-checksum / no-manifest transparency into the final message.
- showImportSnackbar(fragment.getString(
- okNoChecksum ? R.string.install_warn_no_checksum
- : okNoManifest ? R.string.install_warn_manifest_missing
- : R.string.install_msg_import_success));
- });
- }
- } catch (Exception e) {
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> {
- host.setImporting(false);
- stopImportSpinner();
- host.updateDynamicButtons();
- btnImportBackup.setEnabled(true);
- btnImportBackup.setText(fragment.getString(R.string.install_btn_import_backup));
- showImportSnackbar(fragment.getString(R.string.install_msg_import_failed, e.getMessage()));
- });
- }
- } finally {
- host.disableSystemProtection();
- }
- }).start();
- }
-
- private void exportBackupSafely(Uri destUri, String backupFileName) {
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_exporting, backupFileName)).show();
-
- new Thread(() -> {
- host.enableSystemProtection();
- try {
- File sourceFile = new File(new File(fragment.requireContext().getFilesDir(), "rootfs/backups"), backupFileName);
- InputStream is = new java.io.FileInputStream(sourceFile);
- OutputStream os = fragment.requireContext().getContentResolver().openOutputStream(destUri);
- byte[] buffer = new byte[8192];
- int length;
- while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
- }
- os.flush();
- os.close();
- is.close();
-
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> {
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_export_success)).show();
- });
- }
- } catch (Exception e) {
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> {
- Snackbars.make(fragment.getView(), fragment.getString(R.string.install_msg_export_failed, e.getMessage())).show();
- });
- }
- } finally {
- host.disableSystemProtection();
- }
- }).start();
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupHost.java b/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupHost.java
deleted file mode 100644
index 9d4477f23..000000000
--- a/controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupHost.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * ============================================================================
- * Name : BackupHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Narrow seam between DeployFragment and BackupController. The
- * Fragment implements this so the controller (a non-Fragment) can
- * reach the few cross-feature concerns it needs without owning the
- * shared busy-state flags or the all-buttons refresh.
- * ============================================================================
- */
-package org.iiab.controller.backup.presentation;
-
-/** Cross-feature callbacks the BackupController needs from its host Fragment. */
-public interface BackupHost {
- boolean isSystemBusy();
- String getSystemBusyMessage();
- void enableSystemProtection();
- void disableSystemProtection();
- String getTermuxArch();
- /** Re-evaluate enabled/alpha state of ALL deploy buttons (cross-feature). */
- void updateDynamicButtons();
- void setImporting(boolean importing);
- void setRestoring(boolean restoring);
- void setBackupInProgress(boolean inProgress);
- boolean isBackupInProgress();
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/help/TooltipWiring.java b/controller/app/src/main/java/org/iiab/controller/help/TooltipWiring.java
index b33e7ad52..e06737684 100644
--- a/controller/app/src/main/java/org/iiab/controller/help/TooltipWiring.java
+++ b/controller/app/src/main/java/org/iiab/controller/help/TooltipWiring.java
@@ -45,34 +45,9 @@ private TooltipWiring() {}
MAP.put(R.id.nav_about, TooltipTag.SETUP_NAV_ABOUT);
// about
MAP.put(R.id.switch_analytics_consent, TooltipTag.ABOUT_ANALYTICS_CONSENT);
- // dashboard
- MAP.put(R.id.btn_flip_gauges, TooltipTag.DASHBOARD_FLIP_GAUGES);
- MAP.put(R.id.dash_modules_title, TooltipTag.DASHBOARD_MODULES_TITLE);
- // deploy
- MAP.put(R.id.container_led_dcpr, TooltipTag.DEPLOY_LED_DCPR);
- MAP.put(R.id.container_led_ppk, TooltipTag.DEPLOY_LED_PPK);
- MAP.put(R.id.txt_adv_monitoring_title, TooltipTag.DEPLOY_ADV_MONITORING);
- MAP.put(R.id.btn_adb_action, TooltipTag.DEPLOY_ADB_ACTION);
- MAP.put(R.id.btn_tier_basic, TooltipTag.DEPLOY_TIER_BASIC);
- MAP.put(R.id.btn_tier_standard, TooltipTag.DEPLOY_TIER_STANDARD);
- MAP.put(R.id.btn_tier_full, TooltipTag.DEPLOY_TIER_FULL);
- MAP.put(R.id.chk_companion_data, TooltipTag.DEPLOY_COMPANION_DATA);
- MAP.put(R.id.btn_kiwix_settings, TooltipTag.DEPLOY_KIWIX_SETTINGS);
- MAP.put(R.id.btn_fast_install, TooltipTag.DEPLOY_FAST_INSTALL);
- MAP.put(R.id.btn_fast_delete, TooltipTag.DEPLOY_FAST_DELETE);
- MAP.put(R.id.txt_module_mgmt_title, TooltipTag.DEPLOY_MODULE_MGMT);
- MAP.put(R.id.btn_refresh_modules, TooltipTag.DEPLOY_REFRESH_MODULES);
- MAP.put(R.id.btn_launch_install, TooltipTag.DEPLOY_LAUNCH_INSTALL);
- MAP.put(R.id.txt_maintenance_title, TooltipTag.DEPLOY_MAINTENANCE);
- MAP.put(R.id.btn_advanced_backup, TooltipTag.DEPLOY_BACKUP);
- MAP.put(R.id.btn_advanced_reset, TooltipTag.DEPLOY_RESET);
- MAP.put(R.id.btn_advanced_restore, TooltipTag.DEPLOY_RESTORE);
- MAP.put(R.id.btn_advanced_force_stop, TooltipTag.DEPLOY_FORCE_STOP);
- MAP.put(R.id.btn_import_backup, TooltipTag.DEPLOY_IMPORT_BACKUP);
- MAP.put(R.id.txt_select_backup_title, TooltipTag.DEPLOY_SELECT_BACKUP);
- MAP.put(R.id.restore_log_close, TooltipTag.DEPLOY_RESTORE_LOG_CLOSE);
- MAP.put(R.id.col_internet, TooltipTag.DEPLOY_INTERNET);
- MAP.put(R.id.col_dev_mode, TooltipTag.DEPLOY_DEV_MODE);
+ // ADFA-5192: the dashboard / deploy / sync / usage / main tooltip entries were removed with
+ // the legacy tabbed UI (their controls and layouts no longer exist). The redesign wires its
+ // own tooltips; this registry now covers only the surviving native surfaces below.
// feedback
MAP.put(R.id.feedback_category, TooltipTag.FEEDBACK_CATEGORY);
MAP.put(R.id.feedback_send, TooltipTag.FEEDBACK_SEND);
@@ -87,32 +62,6 @@ private TooltipWiring() {}
MAP.put(R.id.language_header, TooltipTag.SETUP_SECTION_LANGUAGE_HEADER);
MAP.put(R.id.spinner_app_language, TooltipTag.SETUP_SECTION_APP_LANGUAGE);
MAP.put(R.id.spinner_language, TooltipTag.SETUP_SECTION_CONTENT_LANGUAGE);
- // sync
- MAP.put(R.id.rb_mode_share, TooltipTag.SYNC_MODE_SHARE);
- MAP.put(R.id.rb_mode_receive, TooltipTag.SYNC_MODE_RECEIVE);
- MAP.put(R.id.rb_net_wifi, TooltipTag.SYNC_NET_WIFI);
- MAP.put(R.id.rb_net_hotspot, TooltipTag.SYNC_NET_HOTSPOT);
- MAP.put(R.id.btn_start_server, TooltipTag.SYNC_START_SERVER);
- MAP.put(R.id.btn_share_app, TooltipTag.SYNC_SHARE_APP);
- MAP.put(R.id.btn_scan_qr, TooltipTag.SYNC_SCAN_QR);
- MAP.put(R.id.btn_cancel_transfer, TooltipTag.SYNC_CANCEL_TRANSFER);
- // usage
- MAP.put(R.id.dash_wifi, TooltipTag.USAGE_WIFI);
- MAP.put(R.id.dash_hotspot, TooltipTag.USAGE_HOTSPOT);
- MAP.put(R.id.btnBrowseContent, TooltipTag.USAGE_BROWSE_CONTENT);
- MAP.put(R.id.setup_dns_check, TooltipTag.USAGE_DNS_CHECK);
- MAP.put(R.id.dns_accept, TooltipTag.USAGE_DNS_ACCEPT);
- MAP.put(R.id.dns_settings_label, TooltipTag.USAGE_DNS_SETTINGS);
- MAP.put(R.id.log_label, TooltipTag.USAGE_LOG);
- MAP.put(R.id.btn_clear_log, TooltipTag.USAGE_CLEAR_LOG);
- MAP.put(R.id.btn_copy_log, TooltipTag.USAGE_COPY_LOG);
- MAP.put(R.id.btn_server_control, TooltipTag.USAGE_SERVER_CONTROL);
- MAP.put(R.id.lohs_toggle, TooltipTag.USAGE_LOHS_TOGGLE);
- MAP.put(R.id.lohs_show_qr, TooltipTag.USAGE_LOHS_SHOW_QR);
- // main
- MAP.put(R.id.btn_share_qr, TooltipTag.MAIN_SHARE_QR);
- MAP.put(R.id.btn_settings, TooltipTag.MAIN_SETTINGS);
- MAP.put(R.id.theme_toggle, TooltipTag.MAIN_THEME_TOGGLE);
}
/** Attach tier-1/2 tooltips (long-press) to every mapped control found under {@code root}. */
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareController.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareController.java
deleted file mode 100644
index 25d8d5fc4..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareController.java
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * ============================================================================
- * Name : AdbShareController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : ADB/Share networking carved out of DeployFragment (strangler-fig,
- * ADFA-4441): ADB connection, NSD discovery, pairing, the ADB UI
- * broadcast receiver and the connection LEDs. Self-contained except
- * for the CPU-chart updates that arrive over the ADB channel
- * (ADB_CPU_UPDATE), routed back to the Fragment via AdbShareHost.
- * Lifecycle hooks (onViewCreated/onResume/onPause) are driven by
- * the Fragment. No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.ColorStateList;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.RemoteInput;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.AdbPairingReceiver;
-import org.iiab.controller.IIABAdbManager;
-import org.iiab.controller.R;
-
-public final class AdbShareController {
-
- private static final String TAG = "IIAB-AdbShareController";
- private static final String SERVICE_TYPE_CONNECT = "_adb-tls-connect._tcp.";
- private static final String SERVICE_TYPE_PAIRING = "_adb-tls-pairing._tcp.";
- private static final String CHANNEL_ID = "adb_pairing_channel";
-
- private final Fragment fragment;
- private final AdbShareHost host;
-
- // Borrowed views (set in onViewCreated()).
- private View ledAdbStatus;
- private View ledDcpr;
- private View ledPpk;
- private TextView txtDcpr;
- private TextView txtPpk;
- private TextView txtAdbLedLabel;
- private Button btnAdbAction;
-
- // Owned ADB/NSD state.
- private android.net.nsd.NsdManager nsdManager;
- private android.net.nsd.NsdManager.DiscoveryListener connectDiscoveryListener, pairingDiscoveryListener;
- private boolean isConnectedToAdb = false, isScanning = false, isAttemptingFastConnect = false;
- private int discoveredConnectPort = -1, discoveredPairingPort = -1;
- private String discoveredHostIp = "127.0.0.1";
- private android.net.wifi.WifiManager.MulticastLock multicastLock;
-
- public AdbShareController(Fragment fragment, AdbShareHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Init from DeployFragment.onViewCreated: stores views + wires ADB. */
- public void onViewCreated(View ledAdbStatus, View ledDcpr, View ledPpk,
- TextView txtDcpr, TextView txtPpk, TextView txtAdbLedLabel, Button btnAdbAction) {
- this.ledAdbStatus = ledAdbStatus;
- this.ledDcpr = ledDcpr;
- this.ledPpk = ledPpk;
- this.txtDcpr = txtDcpr;
- this.txtPpk = txtPpk;
- this.txtAdbLedLabel = txtAdbLedLabel;
- this.btnAdbAction = btnAdbAction;
- nsdManager = (android.net.nsd.NsdManager) fragment.requireContext().getSystemService(Context.NSD_SERVICE);
- setupAdbNetworking();
- setupAdbListeners();
- }
-
- /** From DeployFragment.onResume. */
- public void onResume() {
- registerAdbReceiver();
- }
-
- /** From DeployFragment.onPause. */
- public void onPause() {
- try {
- fragment.requireContext().unregisterReceiver(adbUiUpdateReceiver);
- } catch (Exception ignored) {
- }
- }
-
- private final BroadcastReceiver adbUiUpdateReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
-
- if ("org.iiab.controller.ADB_PAIRING_SUCCESSFUL".equals(action)) {
- fragment.requireContext().getSharedPreferences("iiab_adb_prefs", Context.MODE_PRIVATE)
- .edit().putBoolean("pairing_just_succeeded", false).apply();
-
- android.util.Log.i(TAG, "Broadcast received: Pairing successful! Re-scanning in 2.5s...");
- if (fragment.isAdded()) {
- btnAdbAction.setText(fragment.getString(R.string.adb_status_securing));
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- startAdbPairingFlow();
- }, 2500);
- }
- } else if ("org.iiab.controller.ADB_PAIRING_FAILED".equals(action)) {
- android.util.Log.w(TAG, "Broadcast received: Pairing failed.");
- if (fragment.isAdded()) resetScanState();
-
- } else if ("org.iiab.controller.ADB_PAIRING_SENT".equals(action)) {
- btnAdbAction.setText(fragment.getString(R.string.adb_status_connected));
- isConnectedToAdb = true;
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (fragment.isAdded()) updateUiState(true);
- }, 500);
-
- } else if ("org.iiab.controller.ADB_CPU_UPDATE".equals(action)) {
- String cpuData = intent.getStringExtra("cpu_line");
- if (fragment.isAdded() && isConnectedToAdb && cpuData != null) {
- float cpuVal = host.parseCpuUsage(cpuData);
- if (cpuVal >= 0f) {
- host.addCpuEntry(cpuVal);
- }
- }
- } else if ("org.iiab.controller.ADB_RESTRICTIONS_UPDATE".equals(action)) {
- if (!fragment.isAdded()) return;
-
- String cpValue = intent.getStringExtra("child_process_value");
- String rawPpkValue = intent.getStringExtra("ppk_value");
-
- fragment.requireContext().getSharedPreferences("iiab_adb_prefs", Context.MODE_PRIVATE)
- .edit()
- .putString("child_process_value", cpValue)
- .putString("ppk_value", rawPpkValue)
- .apply();
-
- String ppkDisplay = ("null".equals(rawPpkValue) || "unknown".equals(rawPpkValue)) ? fragment.getString(R.string.adb_ppk_default) : rawPpkValue;
-
- ledDcpr.setBackgroundTintList(null);
- ledPpk.setBackgroundTintList(null);
- ledPpk.setBackgroundResource(R.drawable.led_off);
- ledDcpr.setBackgroundResource(R.drawable.led_off);
-
- if (android.os.Build.VERSION.SDK_INT >= 34) {
- txtPpk.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_ppk_limit_not_required, ppkDisplay), android.text.Html.FROM_HTML_MODE_COMPACT));
- if ("256".equals(rawPpkValue) || "512".equals(rawPpkValue) || "1024".equals(rawPpkValue)) {
- ledPpk.setBackgroundResource(R.drawable.led_on_green);
- } else if ("error".equals(rawPpkValue) || rawPpkValue == null || rawPpkValue.isEmpty()) {
- ledPpk.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_pending)));
- } else {
- ledPpk.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_info)));
- }
-
- if ("0".equals(cpValue) || "false".equals(cpValue)) {
- ledDcpr.setBackgroundResource(R.drawable.led_on_green);
- txtDcpr.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_cp_disabled_ok), android.text.Html.FROM_HTML_MODE_COMPACT));
- } else if ("1".equals(cpValue) || "true".equals(cpValue) || "null".equals(cpValue) || cpValue == null) {
- ledDcpr.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger)));
- txtDcpr.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_cp_enabled_limiting), android.text.Html.FROM_HTML_MODE_COMPACT));
- } else {
- txtDcpr.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_cp_unknown), android.text.Html.FROM_HTML_MODE_COMPACT));
- }
-
- } else if (android.os.Build.VERSION.SDK_INT >= 31) {
- txtDcpr.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_cp_not_required), android.text.Html.FROM_HTML_MODE_COMPACT));
- txtPpk.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_ppk_limit_active, ppkDisplay), android.text.Html.FROM_HTML_MODE_COMPACT));
-
- if ("256".equals(rawPpkValue) || "512".equals(rawPpkValue) || "1024".equals(rawPpkValue)) {
- ledPpk.setBackgroundResource(R.drawable.led_on_green);
- } else if ("error".equals(rawPpkValue) || rawPpkValue == null || rawPpkValue.isEmpty()) {
- ledPpk.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger)));
- } else {
- ledPpk.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_pending)));
- }
- }
- }
- }
- };
-
- private void setupAdbNetworking() {
- android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) fragment.requireContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- if (wifi != null) {
- multicastLock = wifi.createMulticastLock("iiab_adb_multicast_lock");
- multicastLock.setReferenceCounted(true);
- }
- }
-
- private void setupAdbListeners() {
- LinearLayout containerDcpr = fragment.getView().findViewById(R.id.container_led_dcpr);
- containerDcpr.setOnClickListener(v -> {
- if (!isConnectedToAdb) {
- Snackbars.make(v, R.string.adb_req_cp).show();
- return;
- }
- if (android.os.Build.VERSION.SDK_INT < 34) {
- Snackbars.make(v, R.string.adb_not_req_cp).show();
- return;
- }
-
- IIABAdbManager adbManager = IIABAdbManager.getInstance(fragment.requireContext());
- adbManager.executeCommand("settings put global settings_enable_monitor_phantom_procs 0");
- Snackbars.make(v, R.string.adb_snack_disabling_cp).show();
- new Handler(Looper.getMainLooper()).postDelayed(() -> adbManager.checkSystemRestrictions(fragment.requireContext()), 1000);
- });
-
- LinearLayout containerPpk = fragment.getView().findViewById(R.id.container_led_ppk);
- containerPpk.setOnClickListener(v -> {
- if (!isConnectedToAdb) {
- Snackbars.make(v, R.string.adb_req_ppk).show();
- return;
- }
- if (android.os.Build.VERSION.SDK_INT < 31) {
- Snackbars.make(v, R.string.adb_not_req_ppk).show();
- return;
- }
-
- IIABAdbManager adbManager = IIABAdbManager.getInstance(fragment.requireContext());
- adbManager.executeCommand("device_config put activity_manager max_phantom_processes 256");
- Snackbars.make(v, R.string.adb_snack_setting_ppk).show();
- new Handler(Looper.getMainLooper()).postDelayed(() -> adbManager.checkSystemRestrictions(fragment.requireContext()), 1000);
- });
-
- btnAdbAction.setOnClickListener(v -> {
- if (isConnectedToAdb) {
- new Thread(() -> {
- try {
- IIABAdbManager.getInstance(fragment.requireContext()).disconnect();
- } catch (Exception ignored) {
- }
- }).start();
- isConnectedToAdb = false;
- updateUiState(false);
- } else if (!isScanning) {
- startAdbPairingFlow();
- }
- });
- }
-
- private void registerAdbReceiver() {
- IntentFilter filter = new IntentFilter();
- filter.addAction("org.iiab.controller.ADB_PAIRING_SUCCESSFUL");
- filter.addAction("org.iiab.controller.ADB_PAIRING_FAILED");
- filter.addAction("org.iiab.controller.ADB_PAIRING_SENT");
- filter.addAction("org.iiab.controller.ADB_CPU_UPDATE");
- filter.addAction("org.iiab.controller.ADB_RESTRICTIONS_UPDATE");
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
- fragment.requireContext().registerReceiver(adbUiUpdateReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
- } else {
- fragment.requireContext().registerReceiver(adbUiUpdateReceiver, filter);
- }
- }
-
- private void updateUiState(boolean isConnected) {
- btnAdbAction.setEnabled(true);
- if (isConnected) {
- ledAdbStatus.setBackgroundResource(R.drawable.led_on_green);
- txtAdbLedLabel.setText(fragment.getString(R.string.adb_status_connected));
- txtAdbLedLabel.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_success));
- btnAdbAction.setText(fragment.getString(R.string.adb_btn_disconnect));
- btnAdbAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger)));
-
- txtDcpr.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_ui_checking_cp), android.text.Html.FROM_HTML_MODE_COMPACT));
- txtPpk.setText(android.text.Html.fromHtml(fragment.getString(R.string.adb_ui_checking_ppk), android.text.Html.FROM_HTML_MODE_COMPACT));
- ledDcpr.setBackgroundResource(R.drawable.led_off);
- ledDcpr.setBackgroundTintList(null);
- ledPpk.setBackgroundResource(R.drawable.led_off);
- ledPpk.setBackgroundTintList(null);
- } else {
- ledAdbStatus.setBackgroundResource(R.drawable.led_off);
- txtAdbLedLabel.setText(fragment.getString(R.string.adb_status_offline));
- txtAdbLedLabel.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_secondary));
- btnAdbAction.setText(fragment.getString(R.string.adb_btn_connect));
- btnAdbAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_info)));
-
- txtDcpr.setText(fragment.getString(R.string.adb_ui_unknown_cp));
- txtPpk.setText(fragment.getString(R.string.adb_ui_unknown_ppk));
- ledDcpr.setBackgroundResource(R.drawable.led_off);
- ledDcpr.setBackgroundTintList(null);
- ledPpk.setBackgroundResource(R.drawable.led_off);
- ledPpk.setBackgroundTintList(null);
- }
- }
-
- private void startAdbPairingFlow() {
- startAdbPairingFlow(false);
- }
-
- public void startAdbPairingFlow(boolean isSilentScan) {
- isScanning = true;
- isAttemptingFastConnect = false;
- btnAdbAction.setText(fragment.getString(R.string.adb_btn_scanning));
- btnAdbAction.setEnabled(false);
- discoveredConnectPort = -1;
- discoveredPairingPort = -1;
-
- if (multicastLock != null && !multicastLock.isHeld()) multicastLock.acquire();
-
- connectDiscoveryListener = createDiscoveryListener(SERVICE_TYPE_CONNECT);
- pairingDiscoveryListener = createDiscoveryListener(SERVICE_TYPE_PAIRING);
-
- try {
- nsdManager.discoverServices(SERVICE_TYPE_CONNECT, android.net.nsd.NsdManager.PROTOCOL_DNS_SD, connectDiscoveryListener);
- nsdManager.discoverServices(SERVICE_TYPE_PAIRING, android.net.nsd.NsdManager.PROTOCOL_DNS_SD, pairingDiscoveryListener);
- } catch (Exception e) {
- resetScanState();
- return;
- }
-
- if (!isSilentScan) {
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (isScanning && !isConnectedToAdb) openDeveloperOptions();
- }, 4000);
- }
-
- new Handler(Looper.getMainLooper()).postDelayed(this::checkIfScanTimedOut, 90000);
- }
-
- private void attemptFastConnection(String hostIp, int port) {
- if (isAttemptingFastConnect) return;
- isAttemptingFastConnect = true;
-
- Context appContext = fragment.requireContext().getApplicationContext();
- new Thread(() -> {
- boolean connected = false;
- IIABAdbManager adbManager = IIABAdbManager.getInstance(appContext);
-
- for (int i = 0; i < 6; i++) {
- try {
- adbManager.connect(hostIp, port);
- connected = true;
- break;
- } catch (Exception e) {
- try {
- adbManager.disconnect();
- Thread.sleep(600);
- } catch (Exception ignored) {
- }
- }
- }
-
- if (connected) {
- new Handler(Looper.getMainLooper()).post(() -> {
- stopDiscovery();
- isConnectedToAdb = true;
- if (btnAdbAction != null)
- btnAdbAction.setText(fragment.getString(R.string.adb_status_connected));
- updateUiState(true);
- });
- adbManager.startCpuMonitor(appContext);
- adbManager.checkSystemRestrictions(appContext);
- } else {
- isAttemptingFastConnect = false;
- }
- }).start();
- }
-
- private void openDeveloperOptions() {
- try {
- Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- fragment.startActivity(intent);
- } catch (Exception e) {
- Snackbars.make(fragment.getView(), R.string.adb_snack_dev_options).show();
- }
- }
-
- private android.net.nsd.NsdManager.DiscoveryListener createDiscoveryListener(String serviceType) {
- return new android.net.nsd.NsdManager.DiscoveryListener() {
- @Override
- public void onDiscoveryStarted(String regType) {
- }
-
- @Override
- public void onServiceLost(android.net.nsd.NsdServiceInfo service) {
- }
-
- @Override
- public void onDiscoveryStopped(String serviceType) {
- }
-
- @Override
- public void onStartDiscoveryFailed(String serviceType, int errorCode) {
- nsdManager.stopServiceDiscovery(this);
- }
-
- @Override
- public void onStopDiscoveryFailed(String serviceType, int errorCode) {
- nsdManager.stopServiceDiscovery(this);
- }
-
- @Override
- public void onServiceFound(android.net.nsd.NsdServiceInfo service) {
- if (service.getServiceType().contains("_adb-tls")) resolveService(service);
- }
- };
- }
-
- private void resolveService(android.net.nsd.NsdServiceInfo serviceInfo) {
- nsdManager.resolveService(serviceInfo, new android.net.nsd.NsdManager.ResolveListener() {
- @Override
- public void onResolveFailed(android.net.nsd.NsdServiceInfo serviceInfo, int errorCode) {
- }
-
- @Override
- public void onServiceResolved(android.net.nsd.NsdServiceInfo serviceInfo) {
- int port = serviceInfo.getPort();
- String type = serviceInfo.getServiceType();
- String hostIp = serviceInfo.getHost().getHostAddress();
- String myIp = getLocalWifiIp();
-
- if (hostIp != null && !hostIp.equals(myIp) && !hostIp.equals("127.0.0.1")) return;
-
- new Handler(Looper.getMainLooper()).post(() -> {
- discoveredHostIp = hostIp;
- if (type.contains("connect")) {
- discoveredConnectPort = port;
- attemptFastConnection(hostIp, port);
- } else if (type.contains("pairing")) {
- discoveredPairingPort = port;
- }
-
- if (discoveredConnectPort != -1 && discoveredPairingPort != -1 && !isConnectedToAdb) {
- stopDiscovery();
- showPairingNotification(discoveredHostIp, discoveredConnectPort, discoveredPairingPort);
- resetScanState();
- }
- });
- }
- });
- }
-
- private void showPairingNotification(String hostIp, int connectPort, int pairingPort) {
- RemoteInput remoteInput = new RemoteInput.Builder(AdbPairingReceiver.KEY_PIN_REPLY).setLabel(fragment.getString(R.string.adb_notif_input_hint)).build();
- Intent replyIntent = new Intent(fragment.requireContext(), AdbPairingReceiver.class);
- replyIntent.putExtra("hostIp", hostIp);
- replyIntent.putExtra("connectPort", connectPort);
- replyIntent.putExtra("pairingPort", pairingPort);
-
- int flags = PendingIntent.FLAG_UPDATE_CURRENT | (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
- PendingIntent replyPendingIntent = PendingIntent.getBroadcast(fragment.requireContext(), 0, replyIntent, flags);
- NotificationCompat.Action action = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_edit, fragment.getString(R.string.adb_notif_action_pin), replyPendingIntent).addRemoteInput(remoteInput).build();
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "ADB Pairing", NotificationManager.IMPORTANCE_HIGH);
- fragment.requireContext().getSystemService(NotificationManager.class).createNotificationChannel(channel);
- }
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(fragment.requireContext(), CHANNEL_ID)
- .setSmallIcon(android.R.drawable.ic_dialog_info)
- .setContentTitle(fragment.getString(R.string.adb_notif_title))
- .setContentText(fragment.getString(R.string.adb_notif_desc))
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .addAction(action)
- .setAutoCancel(true);
-
- NotificationManager nm = (NotificationManager) fragment.requireContext().getSystemService(Context.NOTIFICATION_SERVICE);
- if (nm != null) nm.notify(AdbPairingReceiver.NOTIFICATION_ID, builder.build());
- }
-
- private void stopDiscovery() {
- try {
- if (connectDiscoveryListener != null) {
- nsdManager.stopServiceDiscovery(connectDiscoveryListener);
- connectDiscoveryListener = null;
- }
- if (pairingDiscoveryListener != null) {
- nsdManager.stopServiceDiscovery(pairingDiscoveryListener);
- pairingDiscoveryListener = null;
- }
- } catch (Exception ignored) {
- } finally {
- if (multicastLock != null && multicastLock.isHeld()) multicastLock.release();
- }
- }
-
- private void checkIfScanTimedOut() {
- if (isScanning && (discoveredConnectPort == -1 || discoveredPairingPort == -1)) {
- Snackbars.make(fragment.getView(), R.string.adb_toast_scan_timeout).show();
- stopDiscovery();
- resetScanState();
- }
- }
-
- private void resetScanState() {
- isScanning = false;
- if (!isConnectedToAdb) {
- btnAdbAction.setEnabled(true);
- btnAdbAction.setText(fragment.getString(R.string.adb_btn_connect));
- }
- }
-
- private String getLocalWifiIp() {
- android.net.wifi.WifiManager wm = (android.net.wifi.WifiManager) fragment.requireContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- if (wm != null) {
- int ip = wm.getConnectionInfo().getIpAddress();
- if (ip != 0)
- return String.format(java.util.Locale.US, "%d.%d.%d.%d", (ip & 0xff), (ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff));
- }
- return "127.0.0.1";
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareHost.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareHost.java
deleted file mode 100644
index 5ac1a3bef..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareHost.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * ============================================================================
- * Name : AdbShareHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between DeployFragment and AdbShareController. The only
- * cross-feature coupling is the CPU chart, fed by the ADB_CPU_UPDATE
- * broadcast that arrives over the ADB channel.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-public interface AdbShareHost {
- float parseCpuUsage(String cpuLine);
- void addCpuEntry(float cpuPercentage);
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallController.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallController.java
deleted file mode 100644
index 9b4cd573f..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallController.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * ============================================================================
- * Name : InstallController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Install UI controller carved out of DeployFragment (strangler-fig,
- * ADFA-4434 PR 2). It owns the install button validations + dialogs,
- * the module install queue (per-role provisioning) and the
- * installation-state verification. The long-running rootfs install
- * pipeline (download + extract + companion data) was moved into the
- * lifecycle-independent foreground InstallService (ADFA-4474 PR2),
- * so this controller only STARTS it and the UI observes progress
- * through InstallProgressRepository. Shared state stays on the
- * Fragment via InstallHost.
- * See controller/docs/TECH_DEBT_PLAN.md.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import org.iiab.controller.config.BoxEndpoints;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.LinearLayout;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.MainActivity;
-import org.iiab.controller.ModuleRegistry;
-import org.iiab.controller.ProgressButton;
-import org.iiab.controller.R;
-import org.iiab.controller.util.LocalVarsYamlParser;
-
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import org.iiab.controller.ui.dialog.BrandDialog;
-
-public final class InstallController {
-
- private final Fragment fragment;
- private final InstallHost host;
-
- private MainActivity mainAct;
- private File debianRootfs;
- private File iiabRootDir;
- private ProgressButton btnFastInstall;
- private ProgressButton btnLaunchInstall;
- private LinearLayout discrepancyWarning;
- private LinearLayout rolesContainer;
- private CheckBox chkCompanionData;
-
- public InstallController(Fragment fragment, InstallHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Wire the install/fast-install buttons. Call from onViewCreated. */
- public void bind(MainActivity mainAct, File debianRootfs, File iiabRootDir,
- ProgressButton btnFastInstall, ProgressButton btnLaunchInstall,
- LinearLayout discrepancyWarning, LinearLayout rolesContainer,
- CheckBox chkCompanionData) {
- this.mainAct = mainAct;
- this.debianRootfs = debianRootfs;
- this.iiabRootDir = iiabRootDir;
- this.btnFastInstall = btnFastInstall;
- this.btnLaunchInstall = btnLaunchInstall;
- this.discrepancyWarning = discrepancyWarning;
- this.rolesContainer = rolesContainer;
- this.chkCompanionData = chkCompanionData;
- bindInstallButtonLogic();
- }
-
- private void bindInstallButtonLogic() {
- btnFastInstall.setOnClickListener(v -> {
- // 1. Main Lock: Server On
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
-
- // 1b. No internet: a fresh install requires downloading the rootfs. Block it
- // up front (but still allow cancelling an in-progress install below).
- if (!host.hasInternet() && !host.isDownloadingRootfs()) {
- Snackbars.make(v, R.string.install_msg_no_connection).show();
- return;
- }
-
- // 2. HIGH PRIORITY: if an install is in flight, this button cancels it.
- // The InstallService handles the cancel and posts the terminal state; the
- // observer in DeployFragment resets the button + shows the snackbar.
- //
- // ADFA-5119, on reachability: no shipping flow gets here. The launcher goes
- // SplashActivity -> LibraryActivity, and the only door left into MainActivity is
- // Settings -> "Terminal (Debian)", which passes EXTRA_TERMINAL_ONLY so ADFA-4987 keeps
- // the legacy dashboard hidden behind the terminal sheet. The button is therefore
- // unreachable — but NOT dead: MainActivity.onCreate builds MainPagerAdapter before it
- // learns it is in terminal-only mode, so DeployFragment is still constructed and still
- // observes InstallProgressRepository from behind the sheet. That is why its phase switch
- // is kept in step with the new phases rather than left to rot, and why sealing this off
- // properly (a terminal host of its own, and the TerminalSessionService notification
- // passing the same extra) is its own piece of work rather than a line in this file.
- if (host.isDownloadingRootfs()
- && InstallProgressRepository.get().currentOp() == InstallState.Op.INSTALL) {
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.install_btn_cancel_title))
- .setMessage(fragment.getString(R.string.install_btn_cancel_msg))
- .setDestructive(fragment.getString(R.string.install_btn_cancel_confirm), () -> {
- Intent cancel = new Intent(fragment.requireContext(), InstallService.class)
- .setAction(InstallService.ACTION_CANCEL);
- fragment.requireContext().startService(cancel);
- })
- .setNegative(fragment.getString(R.string.cancel), null)
- .show();
- return;
- }
-
- // 3. If it is not working, but the system is busy with something else: LOCK
- if (host.isSystemBusy()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
-
- // 4. Normal installation startup validations
- if (host.getSelectedTier() == null) {
- Snackbars.make(v, R.string.install_error_no_tier).show();
- return;
- }
- if (!host.isStorageSafe()) {
- Snackbars.make(v, R.string.install_error_no_storage).show();
- return;
- }
-
- // 5. Start the installation in the foreground service (survives recreation).
- if (debianRootfs.exists() && debianRootfs.isDirectory()) {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.install_btn_reinstall)
- .setMessage(R.string.install_dialog_wipe_msg)
- .setDestructive(R.string.install_btn_yes, () -> startInstallService(true))
- .setNegative(R.string.install_btn_no, null)
- .show();
- } else {
- startInstallService(false);
- }
- });
- }
-
- /** Snapshots the current selections and hands the long-running install to the service. */
- private void startInstallService(boolean reinstall) {
- Context ctx = fragment.requireContext();
- Intent i = new Intent(ctx, InstallService.class);
- i.setAction(InstallService.ACTION_START);
- i.putExtra(InstallService.EXTRA_TIER, host.getSelectedTier() != null ? host.getSelectedTier().name() : null);
- i.putExtra(InstallService.EXTRA_COMPANION, chkCompanionData.isChecked());
- i.putExtra(InstallService.EXTRA_ARCH, host.getTermuxArch());
- i.putExtra(InstallService.EXTRA_KIWIX_LANG, host.getOverrideKiwixLang());
- i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, host.getOverrideKiwixVariant());
- i.putExtra(InstallService.EXTRA_REINSTALL, reinstall);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- ctx.startForegroundService(i);
- } else {
- ctx.startService(i);
- }
- // ADFA-4466 Phase 1: operational analytics (no-op unless the operator opted in).
- org.iiab.controller.analytics.AnalyticsClient.with(ctx).logInstallStarted(
- host.getSelectedTier() != null ? host.getSelectedTier().name() : null,
- chkCompanionData.isChecked(), host.getTermuxArch());
- }
-
- private void evaluateLaunchButton() {
- if (host.isBatchInstalling()) return;
-
- boolean hasSelections = false;
- host.installationQueue().clear();
-
- for (CheckBox cb : host.moduleCheckboxes()) {
- if (cb.isChecked()) {
- hasSelections = true;
- ViewGroup indicatorContainer = (ViewGroup) cb.getParent();
- ViewGroup card = (ViewGroup) indicatorContainer.getParent();
- ModuleRegistry.IiabModule module = (ModuleRegistry.IiabModule) card.getTag();
-
- if (module != null) {
- host.installationQueue().add(module.yamlBaseKey);
- }
- }
- }
-
- btnLaunchInstall.setEnabled(hasSelections);
- btnLaunchInstall.setAlpha(hasSelections ? 1.0f : 0.5f);
- btnLaunchInstall.setText(fragment.getString(R.string.install_btn_launch));
-
- if (hasSelections) {
- btnLaunchInstall.setOnClickListener(v -> {
- MainActivity mainAct = (MainActivity) fragment.getActivity();
- if (mainAct != null && org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
- if (host.isSystemBusy() && !host.isBatchInstalling()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
-
- startModuleQueue();
- });
- } else {
- btnLaunchInstall.setOnClickListener(null);
- }
- }
-
- /**
- * ADFA-4476 slice 3: hand the selected module queue to the foreground InstallService,
- * which owns the dequeue loop (sed/echo/runrole, AnsibleRunOutcome verdict, revert-on-fail)
- * and publishes progress to ModuleQueueRepository. The service, not this Fragment-scoped
- * controller, is the single owner, so a recreation mid-queue cannot launch a second
- * concurrent runrole -- this supersedes the ADFA-4458/4519 re-entry guard and the
- * onResume() re-fire. DeployFragment observes the repository for the grid + snackbars.
- */
- public void startModuleQueue() {
- java.util.List queue = host.installationQueue();
- if (queue == null || queue.isEmpty()) return;
-
- Context ctx = fragment.requireContext();
- Intent i = new Intent(ctx, InstallService.class);
- i.setAction(InstallService.ACTION_START_MODULES);
- i.putExtra(InstallService.EXTRA_MODULES, queue.toArray(new String[0]));
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- ctx.startForegroundService(i);
- } else {
- ctx.startService(i);
- }
-
- host.updateDynamicButtons();
- btnLaunchInstall.setEnabled(false);
- btnLaunchInstall.setText(fragment.getString(R.string.install_btn_launch));
- }
-
- public void fetchLocalVarsFromPRoot() {
- File rootfsDir = new File(fragment.requireContext().getFilesDir(), "rootfs/installed-rootfs/iiab");
- File localVarsFile = new File(rootfsDir, "etc/iiab/local_vars.yml");
-
- if (!rootfsDir.exists() || !rootfsDir.isDirectory() || !localVarsFile.exists()) {
- host.setLastKnownState(new JSONObject());
- verifyInstallationState(host.getLastKnownState());
- return;
- }
-
- new Thread(() -> {
- try {
- StringBuilder yamlOutput = new StringBuilder();
- BufferedReader br = new BufferedReader(new FileReader(localVarsFile));
- String line;
- while ((line = br.readLine()) != null) {
- yamlOutput.append(line).append("\n");
- }
- br.close();
-
- JSONObject freshVars = parseYamlToJson(yamlOutput.toString());
- host.setLastKnownState(freshVars);
-
- if (fragment.getActivity() instanceof MainActivity) {
- fragment.getActivity().getSharedPreferences("iiab_queue_prefs", Context.MODE_PRIVATE)
- .edit().putBoolean("is_module_state_trusted", true).apply();
- }
-
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> verifyInstallationState(freshVars));
- }
- } catch (Exception e) {
- if (fragment.getActivity() != null) {
- fragment.getActivity().runOnUiThread(() -> verifyInstallationState(host.getLastKnownState()));
- }
- }
- }).start();
- }
-
- public void verifyInstallationState(JSONObject jsonVars) {
- new Thread(() -> {
- if (!fragment.isAdded() || fragment.getActivity() == null || rolesContainer == null) return;
-
- boolean isMainServerAlive = host.pingUrl(BoxEndpoints.BASE + "/home");
- boolean discrepancyFound = false;
-
- for (int r = 0; r < rolesContainer.getChildCount(); r++) {
- LinearLayout row = (LinearLayout) rolesContainer.getChildAt(r);
- for (int c = 0; c < row.getChildCount(); c++) {
- LinearLayout card = (LinearLayout) row.getChildAt(c);
- ModuleRegistry.IiabModule module = (ModuleRegistry.IiabModule) card.getTag();
- if (module == null) continue;
-
- android.widget.FrameLayout indicatorContainer = (android.widget.FrameLayout) card.getChildAt(0);
- View led = indicatorContainer.getChildAt(0);
- CheckBox checkBox = (CheckBox) indicatorContainer.getChildAt(1);
-
- boolean isInstallTrue = jsonVars.optBoolean(module.yamlBaseKey + "_install", false);
- boolean isEnabledTrue = jsonVars.optBoolean(module.yamlBaseKey + "_enabled", false);
- boolean yamlState = isInstallTrue || isEnabledTrue;
- boolean pingState = isMainServerAlive && host.pingUrl(BoxEndpoints.BASE + "/" + module.endpoint);
-
- MainActivity mainAct = (MainActivity) fragment.getActivity();
- boolean isRunning = mainAct != null && org.iiab.controller.ServerStateRepository.get().current().alive;
- boolean isTrusted = mainAct != null && mainAct.isModuleStateTrusted();
-
- boolean isConfirmedInstalled;
- boolean isDiscrepancy;
-
- if (isRunning) {
- isConfirmedInstalled = yamlState && pingState;
- isDiscrepancy = yamlState != pingState;
- } else {
- isConfirmedInstalled = yamlState;
- isDiscrepancy = yamlState && !isTrusted;
- }
-
- final boolean finalConfirmed = isConfirmedInstalled;
- final boolean finalDiscrepancyFlag = isDiscrepancy;
- final boolean finalIsRunning = isRunning;
- final String moduleKey = module.yamlBaseKey;
- // ADFA-4519: the app itself is installing this module right now. This is our
- // own authoritative state (survives recreation), so it wins over the yaml read
- // -- otherwise a theme toggle mid-install re-reads local_vars.yml (where
- // '_install: True' was written at runrole START) and falsely shows it done.
- final boolean finalIsInstalling = ModuleQueueRepository.get().isInstalling(moduleKey);
-
- fragment.getActivity().runOnUiThread(() -> {
- card.setOnClickListener(null);
- checkBox.setOnCheckedChangeListener(null);
-
- if (finalIsInstalling) {
- // In progress: keep it as a locked, checked selection -- never "installed".
- led.setVisibility(View.GONE);
- checkBox.setVisibility(View.VISIBLE);
- checkBox.setChecked(true);
- checkBox.setEnabled(false);
- card.setAlpha(0.6f);
- card.setOnClickListener(v -> Snackbars.make(v,
- fragment.getString(R.string.install_status_installing_module, moduleKey)).show());
- } else if (finalConfirmed && !finalDiscrepancyFlag) {
- checkBox.setVisibility(View.GONE);
- led.setVisibility(View.VISIBLE);
- led.setBackgroundTintList(null);
-
- if (finalIsRunning) {
- led.setBackgroundResource(R.drawable.led_on_green);
- card.setOnClickListener(v -> Snackbars.make(v, R.string.install_msg_confirmed).show());
- } else {
- led.setBackgroundResource(R.drawable.led_on_green);
- led.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.accent_secondary)));
- card.setOnClickListener(v -> Snackbars.make(v, R.string.install_msg_offline_trusted).show());
- }
- } else if (finalDiscrepancyFlag) {
- checkBox.setVisibility(View.GONE);
- led.setVisibility(View.VISIBLE);
- led.setBackgroundResource(R.drawable.led_off);
- led.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_pending)));
- card.setOnClickListener(v -> Snackbars.make(v, R.string.install_warning_discrepancy_msg).show());
- } else {
- led.setVisibility(View.GONE);
- checkBox.setVisibility(View.VISIBLE);
- checkBox.setChecked(host.selectedModuleKeys().contains(moduleKey)); // ADFA-4458: restore selection
-
- if (finalIsRunning) {
- checkBox.setEnabled(false);
- card.setAlpha(0.6f);
- card.setOnClickListener(v -> Snackbars.make(v, R.string.install_msg_server_running_lock).show());
- } else {
- checkBox.setEnabled(true);
- card.setAlpha(1.0f);
- checkBox.setButtonTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.text_primary)));
- card.setOnClickListener(v -> checkBox.toggle());
- }
-
- if (!host.moduleCheckboxes().contains(checkBox))
- host.moduleCheckboxes().add(checkBox);
- checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (isChecked) host.selectedModuleKeys().add(moduleKey); else host.selectedModuleKeys().remove(moduleKey);
- evaluateLaunchButton();
- });
- }
- });
-
- if (finalDiscrepancyFlag) discrepancyFound = true;
- }
- }
-
- final boolean finalDiscrepancy = discrepancyFound;
- fragment.getActivity().runOnUiThread(() -> {
- if (discrepancyWarning != null)
- discrepancyWarning.setVisibility(finalDiscrepancy ? View.VISIBLE : View.GONE);
- evaluateLaunchButton();
- });
-
- }).start();
- }
-
- private JSONObject parseYamlToJson(String yaml) {
- // Delegates to the pure, unit-tested util (extracted from this god class).
- // The naive split-on-':' behavior is unchanged; replacing it with a real
- // YAML parser is still tracked as tech-debt D14.
- return LocalVarsYamlParser.parseToJson(yaml);
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallHost.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallHost.java
deleted file mode 100644
index b86b02fbe..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallHost.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * ============================================================================
- * Name : InstallHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between DeployFragment and InstallController. The install
- * pipeline is deeply coupled to fragment-wide state, so that state
- * stays on the Fragment and is reached through this interface;
- * managers (aria2, proot) are exposed here too.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import android.widget.CheckBox;
-
-import org.iiab.controller.Aria2Manager;
-import org.iiab.controller.PRootEngine;
-
-import org.json.JSONObject;
-
-import java.util.List;
-
-public interface InstallHost {
- // selection / projection state (shared with the planner + dynamic buttons)
- org.iiab.controller.InstallationPlanner.Tier getSelectedTier();
- List moduleCheckboxes();
- // ADFA-4476 slice 1: module-grid selection lives in the Activity-scoped
- // ViewModel so it survives recreation; reached through this seam.
- java.util.Set selectedModuleKeys();
- boolean isStorageSafe();
- boolean hasInternet();
- String getOverrideKiwixLang();
- String getOverrideKiwixVariant();
- // install state (kept on the Fragment because other areas read it)
- boolean isDownloadingRootfs();
- void setDownloadingRootfs(boolean v);
- boolean isBatchInstalling();
- void setBatchInstalling(boolean v);
- List installationQueue();
- JSONObject getLastKnownState();
- void setLastKnownState(JSONObject v);
- // shared managers
- Aria2Manager aria2Manager();
- void setAria2Manager(Aria2Manager v);
- PRootEngine prootEngine();
- void setPRootEngine(PRootEngine v);
- // shared helpers / cross-feature
- void updateDynamicButtons();
- String getTermuxArch();
- boolean isSystemBusy();
- String getSystemBusyMessage();
- void enableSystemProtection();
- void disableSystemProtection();
- boolean pingUrl(String url);
- void requestFreshLocalVarsSilently();
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java
deleted file mode 100644
index 0a7311e0b..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java
+++ /dev/null
@@ -1,613 +0,0 @@
-/*
- * ============================================================================
- * Name : PlannerController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Install "planner" presentation logic carved out of DeployFragment
- * (strangler-fig): tier selection, the module checkbox grid, the
- * storage-size projection (RootfsViewModel + observe) and the Kiwix
- * settings dialog. Being a non-Fragment class, it removes this large
- * call graph -- including the LiveData observe() -- from the Fragment
- * that the androidx lint detectors walk. No behaviour change.
- * See controller/docs/TECH_DEBT_PLAN.md (ADFA-4434).
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Typeface;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.InstallationPlanner;
-import org.iiab.controller.MainActivity;
-import org.iiab.controller.ModuleRegistry;
-import org.iiab.controller.MultiResourceGaugeView;
-import org.iiab.controller.R;
-import org.iiab.controller.rootfs.domain.RootfsAbi;
-import org.iiab.controller.rootfs.domain.RootfsTier;
-import org.iiab.controller.rootfs.presentation.RootfsUiState;
-import org.iiab.controller.rootfs.presentation.RootfsViewModel;
-import org.iiab.controller.rootfs.presentation.RootfsViewModelFactory;
-import org.iiab.controller.util.ByteFormatter;
-
-import org.json.JSONObject;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import org.iiab.controller.ui.dialog.BrandDialog;
-
-public final class PlannerController {
-
- private final Fragment fragment;
- private final PlannerHost host;
-
- // Borrowed views (declared on the Fragment; set in bind()).
- private LinearLayout rolesContainer;
- private MultiResourceGaugeView storageGauge;
- private Button btnTierBasic;
- private Button btnTierStandard;
- private Button btnTierFull;
- private TextView txtLegendIiab;
- private TextView txtLegendMaps;
- private TextView txtLegendKiwix;
- private TextView txtLegendFree;
- private TextView txtOfflineEstimate;
- private Button btnKiwixSettings;
- private CheckBox chkCompanionData;
-
- // Owned by the planner (nothing else uses it).
- private RootfsViewModel rootfsViewModel;
-
- public PlannerController(Fragment fragment, PlannerHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Wire the tier buttons, the size-projection observer and the Kiwix dialog. */
- public void bind(LinearLayout rolesContainer, MultiResourceGaugeView storageGauge,
- Button btnTierBasic, Button btnTierStandard, Button btnTierFull,
- TextView txtLegendIiab, TextView txtLegendMaps, TextView txtLegendKiwix,
- TextView txtLegendFree, TextView txtOfflineEstimate,
- Button btnKiwixSettings, CheckBox chkCompanionData) {
- this.rolesContainer = rolesContainer;
- this.storageGauge = storageGauge;
- this.btnTierBasic = btnTierBasic;
- this.btnTierStandard = btnTierStandard;
- this.btnTierFull = btnTierFull;
- this.txtLegendIiab = txtLegendIiab;
- this.txtLegendMaps = txtLegendMaps;
- this.txtLegendKiwix = txtLegendKiwix;
- this.txtLegendFree = txtLegendFree;
- this.txtOfflineEstimate = txtOfflineEstimate;
- this.btnKiwixSettings = btnKiwixSettings;
- this.chkCompanionData = chkCompanionData;
- setupPlannerListeners();
- }
-
- public void createModulesGrid() {
- if (rolesContainer == null || fragment.getContext() == null) return;
- rolesContainer.removeAllViews();
- host.moduleCheckboxes().clear();
-
- boolean isServerRunning = false;
- if (fragment.getActivity() instanceof MainActivity) {
- isServerRunning = org.iiab.controller.ServerStateRepository.get().current().alive;
- }
-
- String termuxArch = host.getTermuxArch();
- boolean is64Bit = termuxArch != null && termuxArch.contains("64");
-
- List activeModules = new ArrayList<>();
- for (ModuleRegistry.IiabModule module : ModuleRegistry.MASTER_ROSTER) {
- if (module.requires64Bit && !is64Bit) continue;
- activeModules.add(module);
- }
-
- int numCols = 3;
- int numRows = (int) Math.ceil((double) activeModules.size() / numCols);
- int ledSizePx = (int) (12 * fragment.getResources().getDisplayMetrics().density);
-
- for (int row = 0; row < numRows; row++) {
- LinearLayout rowLayout = new LinearLayout(fragment.requireContext());
- rowLayout.setOrientation(LinearLayout.HORIZONTAL);
- rowLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- rowLayout.setBaselineAligned(false);
- rowLayout.setWeightSum(numCols);
- rowLayout.setPadding(0, 0, 0, 16);
-
- for (int col = 0; col < numCols; col++) {
- int index = (row * numCols) + col;
- LinearLayout cell = new LinearLayout(fragment.requireContext());
- LinearLayout.LayoutParams cellParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
-
- int margin = 10;
- if (col == 0) cellParams.setMargins(0, 0, margin, 0);
- else if (col == 1) cellParams.setMargins(margin / 2, 0, margin / 2, 0);
- else cellParams.setMargins(margin, 0, 0, 0);
-
- cell.setLayoutParams(cellParams);
-
- if (index < activeModules.size()) {
- ModuleRegistry.IiabModule currentMod = activeModules.get(index);
-
- cell.setOrientation(LinearLayout.HORIZONTAL);
- cell.setBackgroundResource(R.drawable.rounded_button);
- cell.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.dash_module_bg)));
- cell.setPadding(16, 28, 16, 28);
- cell.setGravity(android.view.Gravity.CENTER);
-
- int boxSizePx = (int) (24 * fragment.getResources().getDisplayMetrics().density);
- android.widget.FrameLayout indicatorContainer = new android.widget.FrameLayout(fragment.requireContext());
- LinearLayout.LayoutParams indParams = new LinearLayout.LayoutParams(boxSizePx, boxSizePx);
- indicatorContainer.setLayoutParams(indParams);
-
- View led = new View(fragment.requireContext());
- android.widget.FrameLayout.LayoutParams ledParams = new android.widget.FrameLayout.LayoutParams(ledSizePx, ledSizePx, android.view.Gravity.CENTER);
- led.setLayoutParams(ledParams);
- led.setBackgroundResource(R.drawable.led_off);
-
- CheckBox checkBox = new CheckBox(fragment.requireContext());
- checkBox.setScaleX(0.85f);
- checkBox.setScaleY(0.85f);
- checkBox.setPadding(0, 0, 0, 0);
- android.widget.FrameLayout.LayoutParams cbParams = new android.widget.FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, android.view.Gravity.CENTER);
- checkBox.setLayoutParams(cbParams);
- checkBox.setVisibility(View.GONE);
-
- if (isServerRunning) {
- checkBox.setEnabled(false);
- cell.setAlpha(0.6f);
- } else {
- checkBox.setEnabled(true);
- cell.setAlpha(1.0f);
- }
-
- indicatorContainer.addView(led);
- indicatorContainer.addView(checkBox);
-
- TextView name = new TextView(fragment.requireContext());
- name.setText(fragment.getString(currentMod.nameResId));
- name.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_primary));
- name.setTextSize(12f);
- name.setSingleLine(true);
- LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- textParams.setMargins(16, 0, 0, 0);
- name.setLayoutParams(textParams);
-
- cell.addView(indicatorContainer);
- cell.addView(name);
- cell.setTag(currentMod);
- } else {
- cell.setVisibility(View.INVISIBLE);
- }
- rowLayout.addView(cell);
- }
- rolesContainer.addView(rowLayout);
- }
- }
-
- private void setupPlannerListeners() {
- // Presentation layer: the projection UI consumes the OS rootfs size from
- // RootfsViewModel (live, with offline fallback) instead of having
- // InstallationPlanner resolve it. The observer completes each projection
- // once the size is resolved.
- rootfsViewModel = new ViewModelProvider(fragment, new RootfsViewModelFactory()).get(RootfsViewModel.class);
- rootfsViewModel.state().observe(fragment.getViewLifecycleOwner(), this::onRootfsSizeResolved);
-
- btnTierBasic.setAlpha(0.5f);
- btnTierBasic.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_success)));
- btnTierStandard.setAlpha(0.5f);
- btnTierStandard.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral)));
- btnTierFull.setAlpha(0.5f);
- btnTierFull.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral)));
-
- View.OnClickListener tierClickListener = v -> {
- btnTierBasic.setAlpha(1.0f);
- btnTierStandard.setAlpha(1.0f);
- btnTierFull.setAlpha(1.0f);
- btnTierBasic.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral)));
- btnTierStandard.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral)));
- btnTierFull.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral)));
- v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_success)));
-
- if (v.getId() == R.id.btn_tier_basic) host.setSelectedTier(InstallationPlanner.Tier.BASIC);
- else if (v.getId() == R.id.btn_tier_standard)
- host.setSelectedTier(InstallationPlanner.Tier.STANDARD);
- else if (v.getId() == R.id.btn_tier_full) host.setSelectedTier(InstallationPlanner.Tier.FULL);
-
- host.setOverrideKiwixVariant(null);
- recalculateProjection();
- };
-
- btnTierBasic.setOnClickListener(tierClickListener);
- btnTierStandard.setOnClickListener(tierClickListener);
- btnTierFull.setOnClickListener(tierClickListener);
-
- // ADFA-4474 PR3: restore the companion-data choice (set BEFORE attaching the
- // listener so it does not trigger a redundant recalculation).
- chkCompanionData.setChecked(host.isCompanionData());
- applyKiwixTint(host.isCompanionData() ? R.color.colorAccent : R.color.dash_text_secondary);
-
- chkCompanionData.setOnCheckedChangeListener((buttonView, isChecked) -> {
- host.setCompanionData(isChecked);
- applyKiwixTint(isChecked ? R.color.colorAccent : R.color.dash_text_secondary);
- recalculateProjection();
- });
-
- btnKiwixSettings.setOnClickListener(v -> showKiwixSettingsDialog());
-
- // ADFA-4474 PR3: restore the tier-button highlight from the persisted selection,
- // so the projection gauge + selection survive a recreation. recalculateProjection()
- // below then re-renders the gauge from the restored tier + companion choice.
- restoreTierHighlight();
- recalculateProjection();
- }
-
- /** Re-applies the selected-tier button highlight after a recreation (ADFA-4474 PR3). */
- /** ADFA-4712: tint the labelled "Select content" control (icon + text) by state. */
- private void applyKiwixTint(int colorRes) {
- int c = ContextCompat.getColor(fragment.requireContext(), colorRes);
- btnKiwixSettings.setCompoundDrawableTintList(ColorStateList.valueOf(c));
- btnKiwixSettings.setTextColor(c);
- }
-
- private void restoreTierHighlight() {
- InstallationPlanner.Tier sel = host.getSelectedTier();
- if (sel == null) return;
- btnTierBasic.setAlpha(1.0f);
- btnTierStandard.setAlpha(1.0f);
- btnTierFull.setAlpha(1.0f);
- ColorStateList neutral = ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.btn_neutral));
- ColorStateList success = ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_success));
- btnTierBasic.setBackgroundTintList(neutral);
- btnTierStandard.setBackgroundTintList(neutral);
- btnTierFull.setBackgroundTintList(neutral);
- Button selBtn = (sel == InstallationPlanner.Tier.STANDARD) ? btnTierStandard
- : (sel == InstallationPlanner.Tier.FULL) ? btnTierFull : btnTierBasic;
- selBtn.setBackgroundTintList(success);
- }
-
- private void recalculateProjection() {
- InstallationPlanner.Tier evalTier = (host.getSelectedTier() != null) ? host.getSelectedTier() : InstallationPlanner.Tier.BASIC;
- // Ask the presentation layer for the OS rootfs size. onRootfsSizeResolved()
- // (registered as an observer in setupPlannerListeners) reacts and finishes
- // the projection with the resolved size.
- if (rootfsViewModel != null) {
- // When we already know we're offline, skip the live fetch (avoids the ~6s
- // network timeout) and go straight to the hardcoded fallback size.
- rootfsViewModel.load(toRootfsTier(evalTier), detectRootfsAbi(), host.hasInternet());
- }
- }
-
- /** ADFA-5105: the wizard tier -> the rootfs slice's tier enum (same names, null -> BASIC). */
- private static org.iiab.controller.rootfs.domain.RootfsTier mapRootfsTier(org.iiab.controller.InstallationPlanner.Tier t) {
- if (t == null) return org.iiab.controller.rootfs.domain.RootfsTier.BASIC;
- switch (t) {
- case FULL: return org.iiab.controller.rootfs.domain.RootfsTier.FULL;
- case STANDARD: return org.iiab.controller.rootfs.domain.RootfsTier.STANDARD;
- default: return org.iiab.controller.rootfs.domain.RootfsTier.BASIC;
- }
- }
-
- private void onRootfsSizeResolved(RootfsUiState rootfsState) {
- if (!fragment.isAdded() || rootfsState == null) return;
- if (rootfsState.status == RootfsUiState.Status.LOADING) return;
-
- final double osGiB = (rootfsState.rootfs != null)
- ? ByteFormatter.toGiB(rootfsState.rootfs.sizeBytes())
- : 0.0;
-
- // Show the "estimated (offline)" caption whenever the size is a fallback
- // (no live value), so the user knows the projection isn't server-confirmed.
- if (txtOfflineEstimate != null) {
- txtOfflineEstimate.setVisibility(rootfsState.live ? View.GONE : View.VISIBLE);
- }
-
- android.content.SharedPreferences prefs = fragment.requireContext().getSharedPreferences(fragment.getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
- String targetLang = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault());
- InstallationPlanner.Tier evalTier = (host.getSelectedTier() != null) ? host.getSelectedTier() : InstallationPlanner.Tier.BASIC;
-
- InstallationPlanner.calculateProjectedSize(fragment.requireContext(), evalTier, chkCompanionData.isChecked(), targetLang, host.getOverrideKiwixVariant(), osGiB, new InstallationPlanner.PlanResultListener() {
- @Override
- public void onCalculated(InstallationPlanner.StorageProjection projection) {
- if (!fragment.isAdded()) return;
-
- File path = android.os.Environment.getDataDirectory();
- double freeSpaceGb = path.getFreeSpace() / (1024.0 * 1024.0 * 1024.0);
- double totalSpaceGb = path.getTotalSpace() / (1024.0 * 1024.0 * 1024.0);
- double usedSpaceGb = totalSpaceGb - freeSpaceGb;
-
- final double GB = 1024.0 * 1024.0 * 1024.0;
- boolean hasTier = host.getSelectedTier() != null;
- // --- DETECT ARCHITECTURE ---
- String arch = host.getTermuxArch();
- boolean is64Bit = arch != null && arch.contains("64");
-
- // ADFA-5105: size the OS by its UNCOMPRESSED footprint (what actually lands), the
- // same figure the destructive gate uses — not the compressed download — so the
- // legend and the "fits" decision agree with the hard preflight in InstallService.
- org.iiab.controller.rootfs.data.RootfsCatalog rc =
- new org.iiab.controller.rootfs.data.RootfsCatalog(fragment.requireContext());
- org.iiab.controller.rootfs.domain.RootfsAbi rAbi = rc.detectAbi();
- org.iiab.controller.rootfs.domain.RootfsTier rTier = mapRootfsTier(host.getSelectedTier());
-
- double pOs = hasTier ? rc.installedBytes(rTier, rAbi) / GB : 0.0;
- double pMaps = hasTier ? projection.mapsSize : 0.0;
-
- // --- FORCE KIWIX TO ZERO IN 32-BITS (change if kiwix gets support for 32bits somehow) ---
- double pKiwix = (!hasTier || !is64Bit) ? 0.0 : projection.kiwixSize;
- double pTotal = pOs + pMaps + pKiwix;
-
- // Gate on the same PEAK the install needs — the compressed download and the
- // uncompressed tree coexist during extraction (RootfsCatalog.peakInstallBytes) — plus
- // the content, on the real write target. UNKNOWN free space stays "safe" (advisory);
- // the hard preflight in InstallService refuses for real before any wipe.
- long neededBytes = (hasTier ? rc.peakInstallBytes(rTier, rAbi) : 0L)
- + (long) Math.ceil((pMaps + pKiwix) * GB);
- Long freeBytes = org.iiab.controller.storage.StorageProbe.freeBytes(fragment.requireContext());
- host.setStorageSafe(org.iiab.controller.storage.StorageGuard.evaluate(freeBytes, neededBytes)
- != org.iiab.controller.storage.StorageGuard.Verdict.DOES_NOT_FIT);
-
- if (txtLegendIiab != null)
- txtLegendIiab.setText(String.format(java.util.Locale.US, "%.1fG", pOs));
- if (txtLegendMaps != null)
- txtLegendMaps.setText(String.format(java.util.Locale.US, "%.1fG", pMaps));
- if (txtLegendKiwix != null)
- txtLegendKiwix.setText(String.format(java.util.Locale.US, "%.1fG", pKiwix));
-
- TextView lblWiki = fragment.getView().findViewById(R.id.txt_legend_kiwix).getRootView().findViewWithTag("label_kiwix");
- if (lblWiki == null && txtLegendKiwix != null) {
- ViewGroup parent = (ViewGroup) txtLegendKiwix.getParent();
- lblWiki = (TextView) parent.getChildAt(1);
- }
- // --- HIDE UI OF KIWIX IF IT IS 32-BITS ---
- if (!is64Bit) {
- // We force "N/A" in the size text
- if (txtLegendKiwix != null) {
- txtLegendKiwix.setText(fragment.getString(R.string.install_msg_backup_na)); // Use the "N/A" string you already have
- txtLegendKiwix.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_secondary));
- }
-
- if (lblWiki != null) {
- lblWiki.setText(fragment.getString(R.string.install_legend_wiki_plain));
- // We apply gray
- lblWiki.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_secondary));
- // (Optional) We can cross it out to make it clear that it is disabled
- lblWiki.setPaintFlags(lblWiki.getPaintFlags() | android.graphics.Paint.STRIKE_THRU_TEXT_FLAG);
- }
-
- // We hide the gear so it cannot interact
- if (btnKiwixSettings != null) btnKiwixSettings.setVisibility(View.GONE);
- } else if (lblWiki != null) {
- // We clean the strikethrough (in case the view is recycled)
- lblWiki.setPaintFlags(lblWiki.getPaintFlags() & (~android.graphics.Paint.STRIKE_THRU_TEXT_FLAG));
-
- // Normal logic for 64-bit
- if (chkCompanionData.isChecked()) {
- lblWiki.setText(fragment.getString(R.string.install_legend_wiki_lang, projection.resolvedLang.toUpperCase()));
- lblWiki.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_info));
- } else {
- lblWiki.setText(fragment.getString(R.string.install_legend_wiki_plain));
- lblWiki.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_secondary));
- }
- }
-
- if (txtLegendFree != null) {
- if (host.isStorageSafe()) {
- txtLegendFree.setText(String.format(java.util.Locale.US, "%.1fG", (freeSpaceGb - pTotal)));
- txtLegendFree.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_inverted));
- } else {
- txtLegendFree.setText(R.string.storage_overload);
- txtLegendFree.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger));
- }
- }
-
- if (storageGauge != null) {
- List segments = new ArrayList<>();
- float otherUsedPct = (totalSpaceGb > 0) ? (float) (usedSpaceGb / totalSpaceGb) * 100f : 0f;
- float osPct = (totalSpaceGb > 0) ? (float) (pOs / totalSpaceGb) * 100f : 0f;
- float mapsPct = (totalSpaceGb > 0) ? (float) (pMaps / totalSpaceGb) * 100f : 0f;
- float kiwixPct = (totalSpaceGb > 0) ? (float) (pKiwix / totalSpaceGb) * 100f : 0f;
- float totalDrawn = 0f;
-
- if (otherUsedPct > 0) {
- float draw = Math.min(otherUsedPct, 100f - totalDrawn);
- segments.add(new MultiResourceGaugeView.Segment(draw, ContextCompat.getColor(fragment.requireContext(), R.color.chart_track)));
- totalDrawn += draw;
- }
- if (osPct > 0 && totalDrawn < 100f) {
- float draw = Math.min(osPct, 100f - totalDrawn);
- segments.add(new MultiResourceGaugeView.Segment(draw, ContextCompat.getColor(fragment.requireContext(), R.color.chart_os)));
- totalDrawn += draw;
- }
- if (mapsPct > 0 && totalDrawn < 100f) {
- float draw = Math.min(mapsPct, 100f - totalDrawn);
- segments.add(new MultiResourceGaugeView.Segment(draw, ContextCompat.getColor(fragment.requireContext(), R.color.chart_maps)));
- totalDrawn += draw;
- }
- if (kiwixPct > 0 && totalDrawn < 100f) {
- float draw = Math.min(kiwixPct, 100f - totalDrawn);
- segments.add(new MultiResourceGaugeView.Segment(draw, ContextCompat.getColor(fragment.requireContext(), R.color.chart_wiki)));
- }
-
- int centerColor = (host.getSelectedTier() == null || host.isStorageSafe()) ? ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_inverted) : ContextCompat.getColor(fragment.requireContext(), R.color.status_danger);
- storageGauge.updateData(segments, String.format(java.util.Locale.US, "%.1fG", pTotal), centerColor, fragment.getString(R.string.gauge_projected), fragment.getString(R.string.gauge_storage));
- }
-
- if (fragment.getActivity() != null)
- fragment.getActivity().runOnUiThread(() -> host.updateDynamicButtons());
- }
-
- @Override
- public void onError(String error) {
- if (fragment.isAdded() && txtLegendFree != null) txtLegendFree.setText(R.string.gauge_error);
- }
- });
- }
-
- private void showKiwixSettingsDialog() {
- View view = fragment.getLayoutInflater().inflate(R.layout.dialog_install_planner_settings, null);
- BrandDialog.Handle dialog = new BrandDialog(fragment.requireContext()).setContentView(view).create();
-
- final android.widget.Spinner spinnerLang = view.findViewById(R.id.spinner_kiwix_lang);
- final Button btnWipe = view.findViewById(R.id.btn_wipe_cache);
- final Button btnSelect = view.findViewById(R.id.btn_select_variant);
- final android.widget.RadioGroup rgVariants = view.findViewById(R.id.rg_kiwix_variants);
- final View content = view.findViewById(R.id.planner_content);
- final View loading = view.findViewById(R.id.planner_loading);
- final View offline = view.findViewById(R.id.planner_offline);
- final View btnClose = view.findViewById(R.id.btn_planner_close);
- final Button btnRetry = view.findViewById(R.id.btn_planner_retry);
-
- btnClose.setOnClickListener(v -> dialog.dismiss());
-
- // Load (or reload) the Kiwix catalog from the Internet, swapping the dialog
- // between loading / offline / ready. Open, Retry and Wipe Cache all reuse this
- // one path so the dialog is never left empty or stranded (ADFA-4658).
- final Runnable[] loadRef = new Runnable[1];
- loadRef[0] = () -> {
- loading.setVisibility(View.VISIBLE);
- content.setVisibility(View.GONE);
- offline.setVisibility(View.GONE);
- btnSelect.setEnabled(false);
-
- InstallationPlanner.getOrFetchCatalog(fragment.requireContext(), new InstallationPlanner.CacheListener() {
- @Override
- public void onReady(JSONObject catalog) {
- if (!fragment.isAdded()) return;
- loading.setVisibility(View.GONE);
- offline.setVisibility(View.GONE);
- content.setVisibility(View.VISIBLE);
- btnSelect.setEnabled(true);
-
- List langKeys = new ArrayList<>();
- java.util.Iterator keys = catalog.keys();
- while (keys.hasNext()) langKeys.add(keys.next());
- java.util.Collections.sort(langKeys);
-
- List displayNames = new ArrayList<>();
- int selectedIndex = 0;
- android.content.SharedPreferences prefs = fragment.requireContext().getSharedPreferences(fragment.getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
- String currentTarget = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault());
-
- for (int i = 0; i < langKeys.size(); i++) {
- String code = langKeys.get(i);
- java.util.Locale loc = new java.util.Locale(code);
- String name = loc.getDisplayLanguage(loc);
- displayNames.add(name.substring(0, 1).toUpperCase() + name.substring(1) + " / " + loc.getDisplayLanguage(java.util.Locale.US));
- if (code.equals(currentTarget)) selectedIndex = i;
- }
-
- android.widget.ArrayAdapter adapter = new android.widget.ArrayAdapter<>(fragment.requireContext(), android.R.layout.simple_spinner_dropdown_item, displayNames);
- spinnerLang.setAdapter(adapter);
- spinnerLang.setSelection(selectedIndex);
-
- spinnerLang.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(android.widget.AdapterView> parent, View v, int position, long id) {
- String selectedCode = langKeys.get(position);
- rgVariants.removeAllViews();
-
- JSONObject variants = catalog.optJSONObject(selectedCode);
- if (variants != null) {
- java.util.Iterator vKeys = variants.keys();
- while (vKeys.hasNext()) {
- String vk = vKeys.next();
- JSONObject vData = variants.optJSONObject(vk);
- double size = (vData != null) ? vData.optDouble("size", 0.0) : 0.0;
-
- android.widget.RadioButton rb = new android.widget.RadioButton(fragment.requireContext());
- rb.setId(View.generateViewId());
- rb.setText(String.format(java.util.Locale.US, "%-22s %5.1f GB", vk, size));
- rb.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.dash_text_primary));
- rb.setTypeface(Typeface.MONOSPACE);
- rb.setTag(vk);
- rgVariants.addView(rb);
-
- if (vk.equals(host.getOverrideKiwixVariant())) rb.setChecked(true);
- }
- }
- }
-
- @Override
- public void onNothingSelected(android.widget.AdapterView> parent) {
- }
- });
-
- btnSelect.setOnClickListener(v -> {
- int checkedId = rgVariants.getCheckedRadioButtonId();
- android.widget.RadioButton rb = (checkedId != -1) ? rgVariants.findViewById(checkedId) : null;
- int pos = spinnerLang.getSelectedItemPosition();
- if (rb != null && pos >= 0 && pos < langKeys.size()) {
- host.setOverrideKiwixVariant((String) rb.getTag());
- host.setOverrideKiwixLang(langKeys.get(pos));
- recalculateProjection();
- dialog.dismiss();
- } else {
- Snackbars.make(fragment.getView(), R.string.kiwix_select_variant_error).show();
- }
- });
- }
-
- @Override
- public void onError(String error) {
- if (!fragment.isAdded()) return;
- loading.setVisibility(View.GONE);
- content.setVisibility(View.GONE);
- offline.setVisibility(View.VISIBLE);
- btnSelect.setEnabled(false);
- }
- });
- };
-
- // Wipe Cache: clear the stale selection (clearCheck BEFORE removeAllViews, or
- // the RadioGroup keeps its checked id and Select crashes on a null RadioButton,
- // ADFA-4658), then reload the catalog. It does NOT close the dialog.
- btnWipe.setOnClickListener(v -> {
- InstallationPlanner.wipeCache(fragment.requireContext());
- rgVariants.clearCheck();
- rgVariants.removeAllViews();
- spinnerLang.setAdapter(null);
- host.setOverrideKiwixVariant(null);
- Snackbars.make(fragment.getView(), R.string.kiwix_cache_wiped).show();
- loadRef[0].run();
- });
-
- if (btnRetry != null) btnRetry.setOnClickListener(v -> loadRef[0].run());
-
- loadRef[0].run();
- dialog.show();
- }
-
- private RootfsTier toRootfsTier(InstallationPlanner.Tier tier) {
- switch (tier) {
- case STANDARD:
- return RootfsTier.STANDARD;
- case FULL:
- return RootfsTier.FULL;
- case BASIC:
- default:
- return RootfsTier.BASIC;
- }
- }
-
- private RootfsAbi detectRootfsAbi() {
- String arch = host.getTermuxArch();
- return (arch != null && arch.contains("64")) ? RootfsAbi.ARM64_V8A : RootfsAbi.ARMEABI_V7A;
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerHost.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerHost.java
deleted file mode 100644
index e6ef8614f..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerHost.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * ============================================================================
- * Name : PlannerHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between DeployFragment and PlannerController. "What to
- * install" state (tier, module checkboxes, kiwix overrides) stays
- * on the Fragment because updateDynamicButtons + the install-action
- * also read it; the controller reaches it through this interface.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import android.widget.CheckBox;
-
-import java.util.List;
-
-public interface PlannerHost {
- org.iiab.controller.InstallationPlanner.Tier getSelectedTier();
- void setSelectedTier(org.iiab.controller.InstallationPlanner.Tier tier);
- boolean isCompanionData();
- void setCompanionData(boolean companionData);
- List moduleCheckboxes();
- void setStorageSafe(boolean safe);
- boolean isStorageSafe();
- String getOverrideKiwixLang();
- void setOverrideKiwixLang(String lang);
- String getOverrideKiwixVariant();
- void setOverrideKiwixVariant(String variant);
- boolean hasInternet();
- void updateDynamicButtons();
- String getTermuxArch();
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteController.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteController.java
deleted file mode 100644
index 450d96bca..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteController.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * ============================================================================
- * Name : ResetDeleteController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Reset (wipe + reset the installed rootfs) and Delete/uninstall
- * actions carved out of DeployFragment (strangler-fig, ADFA-4440).
- * Destructive rootfs ops; cohesive. Shared state stays on the
- * Fragment via ResetDeleteHost; managers (aria2, proot) via host
- * accessors. No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import android.util.Log;
-import android.widget.Button;
-
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.MainActivity;
-import org.iiab.controller.ProgressButton;
-import org.iiab.controller.R;
-import org.iiab.controller.util.ProcessRunner;
-
-import java.io.File;
-import org.iiab.controller.ui.dialog.BrandDialog;
-
-public final class ResetDeleteController {
-
- private static final String TAG = "IIAB-ResetDeleteController";
-
- private final Fragment fragment;
- private final ResetDeleteHost host;
-
- private MainActivity mainAct;
- private File debianRootfs;
- private Button btnAdvancedReset;
- private ProgressButton btnFastDelete;
-
- public ResetDeleteController(Fragment fragment, ResetDeleteHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Wire the reset + delete buttons. Call from onViewCreated. */
- public void bind(MainActivity mainAct, File debianRootfs,
- Button btnAdvancedReset, ProgressButton btnFastDelete) {
- this.mainAct = mainAct;
- this.debianRootfs = debianRootfs;
- this.btnAdvancedReset = btnAdvancedReset;
- this.btnFastDelete = btnFastDelete;
- bindDeleteButtonLogic();
- bindResetButtonLogic();
- }
-
- private void bindResetButtonLogic() {
- if (btnAdvancedReset == null) return;
- btnAdvancedReset.setOnClickListener(v -> {
- InstallProgressRepository repo = InstallProgressRepository.get();
-
- // If a reset is already in flight, this tap cancels it (the button text
- // invites "Tap to Cancel" during the download phase).
- if (repo.isRunning() && repo.currentOp() == InstallState.Op.RESET) {
- fragment.requireContext().startService(
- new android.content.Intent(fragment.requireContext(), InstallService.class)
- .setAction(InstallService.ACTION_CANCEL));
- return;
- }
-
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
- // isSystemBusy() covers an install (or any other long op) in flight.
- if (host.isSystemBusy()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
- // NORMAL STATE: RESET START -> hand the pipeline to InstallService.
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.install_dialog_reset_title)
- .setMessage(R.string.install_dialog_reset_msg)
- .setDestructive(R.string.install_dialog_reset_confirm, () -> {
- mainAct.invalidateModuleStateTrust();
- android.content.Context ctx = fragment.requireContext();
- android.content.Intent i = new android.content.Intent(ctx, InstallService.class);
- i.setAction(InstallService.ACTION_START);
- i.putExtra(InstallService.EXTRA_MODE, InstallService.MODE_RESET);
- i.putExtra(InstallService.EXTRA_ARCH, host.getTermuxArch());
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- ctx.startForegroundService(i);
- } else {
- ctx.startService(i);
- }
- })
- .setNegative(R.string.install_dialog_reset_cancel, null)
- .show();
- });
- }
-
- /**
- * ADFA-5070: the legacy "fast delete" is switched off, not deleted.
- *
- *
It is the only destructive route that removes the rootfs without taking the
- * shared {@code EnvironmentLock} or planting {@code InstallGuard}'s marker. It
- * has its own ad-hoc gating — server alive, host busy, a local protection flag —
- * but because it holds neither shared lock it is invisible to every other flow,
- * and a delete killed half-way leaves no marker, so the boot check cannot detect
- * the wreckage it leaves.
- *
- *
It is also legacy surface: {@code ResetDeleteController} is instantiated
- * only by {@code DeployFragment}, the pre-redesign god class. The current flow
- * reaches the same outcome through the wizard's reinstall, which is guarded.
- *
- *
One flag rather than a deletion, so turning it back on for debugging is a
- * one-line change and the code stays readable while the screen is retired.
- */
- private static final boolean LEGACY_FAST_DELETE_ENABLED = false;
-
- private void bindDeleteButtonLogic() {
- if (!LEGACY_FAST_DELETE_ENABLED) {
- // Hidden rather than disabled: updateDynamicButtons re-enables buttons and
- // resets their alpha, but never touches visibility, so this survives.
- btnFastDelete.setVisibility(android.view.View.GONE);
- return;
- }
- btnFastDelete.setOnClickListener(v -> {
- if (org.iiab.controller.ServerStateRepository.get().current().alive) {
- Snackbars.make(v, R.string.install_msg_server_running_lock).show();
- return;
- }
- if (host.isSystemBusy()) {
- Snackbars.make(v, host.getSystemBusyMessage()).show();
- return;
- }
-
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.install_dialog_delete_title)
- .setMessage(R.string.install_dialog_delete_msg)
- .setDestructive(R.string.install_btn_delete_confirm, () -> {
- host.setDeleting(true);
- mainAct.runOnUiThread(host::updateDynamicButtons);
-
- mainAct.invalidateModuleStateTrust();
- btnFastDelete.setEnabled(false);
- btnFastDelete.startProgress();
- Snackbars.make(fragment.getView(), R.string.install_status_deleting).show();
- new Thread(() -> {
- host.enableSystemProtection();
- try {
- // ADFA-5070: unreachable while LEGACY_FAST_DELETE_ENABLED is
- // false, and wired anyway — flipping the flag back on must
- // not also bring back the stale state it used to leave.
- // mainAct rather than fragment.requireContext(): this runs on a
- // worker thread and the fragment may have detached by now.
- org.iiab.controller.system.data.ContentStateInvalidator
- .replacementStarting(mainAct,
- org.iiab.controller.system.domain
- .SystemReplacement.Cause.DELETE);
- ProcessRunner.Result wipeResult = ProcessRunner.run(new String[]{"rm", "-rf", debianRootfs.getAbsolutePath()});
- if (!wipeResult.isSuccess()) {
- Log.w(TAG, "rm -rf rootfs (delete) failed (exit " + wipeResult.exitCode + "): " + wipeResult.output);
- } else {
- org.iiab.controller.system.data.ContentStateInvalidator
- .replacementSucceeded(mainAct,
- org.iiab.controller.system.domain
- .SystemReplacement.Cause.DELETE);
- }
- } catch (Exception e) {
- mainAct.runOnUiThread(() -> Snackbars.make(fragment.getView(), fragment.getString(R.string.install_error_delete, e.getMessage())).show());
- } finally {
- host.setDeleting(false);
- mainAct.runOnUiThread(() -> { btnFastDelete.stopProgress(); host.updateDynamicButtons(); });
- host.disableSystemProtection();
- }
- }).start();
- })
- .setNegative(R.string.cancel, null)
- .show();
- });
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteHost.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteHost.java
deleted file mode 100644
index 855dcb339..000000000
--- a/controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteHost.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * ============================================================================
- * Name : ResetDeleteHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between DeployFragment and ResetDeleteController. Most state
- * is shared with the install flow and stays on the Fragment.
- * ============================================================================
- */
-package org.iiab.controller.install.presentation;
-
-import org.iiab.controller.Aria2Manager;
-import org.iiab.controller.PRootEngine;
-
-public interface ResetDeleteHost {
- void updateDynamicButtons();
- String getTermuxArch();
- boolean isSystemBusy();
- String getSystemBusyMessage();
- void enableSystemProtection();
- void disableSystemProtection();
- boolean isDeleting();
- void setDeleting(boolean v);
- boolean isDownloadingRootfs();
- void setDownloadingRootfs(boolean v);
- Aria2Manager aria2Manager();
- void setAria2Manager(Aria2Manager v);
- PRootEngine prootEngine();
- void setPRootEngine(PRootEngine v);
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java
index 666d01f42..d4ed7f6f4 100644
--- a/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java
@@ -506,7 +506,7 @@ private boolean rootfsPresent() {
/**
* This app's ACTUAL installed ABI width, read from nativeLibraryDir — not the device's 64-bit
* capability. A 32-bit install on a 64-bit phone must report 32, because the rootfs/library arch
- * follows the app's install ABI, not the hardware. Mirrors ArchCheckController.getArchBits().
+ * follows the app's install ABI, not the hardware.
* (ADFA-4784: the earlier Build.SUPPORTED_64_BIT_ABIS check wrongly passed 32-on-64 as compatible.)
*/
private int archBits() {
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckController.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckController.java
deleted file mode 100644
index ab01e3676..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckController.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * ============================================================================
- * Name : ArchCheckController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Architecture (32/64-bit) compatibility check carved out of
- * SyncFragment (strangler-fig, ADFA-4506): the host/guest arch
- * labels, the incompatibility dialog, and the compatibility-success
- * feedback (vibrate + snackbar + delayed label reset). Self-contained;
- * the label-visibility rule reads the sync mode / server state back
- * through ArchCheckHost. No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.View;
-import android.widget.TextView;
-
-import org.iiab.controller.ui.dialog.BrandDialog;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.snackbar.Snackbar;
-import org.iiab.controller.util.Snackbars;
-
-import org.iiab.controller.R;
-
-public final class ArchCheckController {
-
- private static final String TAG = "IIAB-ArchCheckController";
-
- private final Fragment fragment;
- private final ArchCheckHost host;
-
- // Borrowed views (set in bind(), from the Fragment's onCreateView()).
- private TextView txtHostArchLabel;
- private TextView txtGuestArchLabel;
-
- public ArchCheckController(Fragment fragment, ArchCheckHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Borrow the two arch-label views owned by the Fragment's layout. */
- public void bind(TextView hostArchLabel, TextView guestArchLabel) {
- this.txtHostArchLabel = hostArchLabel;
- this.txtGuestArchLabel = guestArchLabel;
- }
-
- /** This device's architecture width in bits (64 if the native lib dir is 64-bit). */
- public int getArchBits() {
- String arch = getTermuxArch();
- return (arch != null && arch.contains("64")) ? 64 : 32;
- }
-
- /** Set the static "App is N-bit" text on both labels. */
- public void applyStaticLabels() {
- String archLabelText = fragment.getString(R.string.sync_app_arch_label, getArchBits());
- if (txtHostArchLabel != null) txtHostArchLabel.setText(archLabelText);
- if (txtGuestArchLabel != null) txtGuestArchLabel.setText(archLabelText);
- }
-
- public void showArchIncompatibilityDialog(String message) {
- android.os.Vibrator v = (android.os.Vibrator) fragment.requireContext().getSystemService(Context.VIBRATOR_SERVICE);
- if (v != null) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- v.vibrate(android.os.VibrationEffect.createOneShot(500, android.os.VibrationEffect.DEFAULT_AMPLITUDE));
- } else {
- v.vibrate(500);
- }
- }
-
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.sync_error_arch_title))
- .setMessage(message)
- .setPositive(fragment.getString(R.string.adb_enforcer_btn_ok), null)
- .show();
- }
-
- private String getTermuxArch() {
- try {
- android.content.pm.ApplicationInfo info = fragment.requireContext().getApplicationInfo();
- String nativeLibDir = info.nativeLibraryDir;
- if (nativeLibDir != null) {
- if (nativeLibDir.endsWith("arm64") || nativeLibDir.contains("arm64-v8a"))
- return "arm64-v8a";
- if (nativeLibDir.endsWith("arm") || nativeLibDir.contains("armeabi-v7a"))
- return "armeabi-v7a";
- if (nativeLibDir.endsWith("x86_64") || nativeLibDir.contains("x86_64"))
- return "x86_64";
- if (nativeLibDir.endsWith("x86") || nativeLibDir.contains("x86")) return "x86";
- }
- } catch (Exception e) {
- Log.w(TAG, "Failed to read native library dir for arch detection", e);
- }
- if (android.os.Build.SUPPORTED_ABIS.length > 0) return android.os.Build.SUPPORTED_ABIS[0];
- return "unknown";
- }
-
- public void showArchCompatibilitySuccess(Runnable onComplete) {
- // S8: this runs in the pre-transfer probing phase. A theme toggle / config
- // change here detaches the fragment, so guard every context/view access and
- // re-check inside the delayed runnable before touching the UI (was an
- // IllegalStateException "not attached to a context" crash).
- Context ctx = fragment.getContext();
- if (!fragment.isAdded() || ctx == null || fragment.getView() == null) return;
-
- android.os.Vibrator v = (android.os.Vibrator) ctx.getSystemService(Context.VIBRATOR_SERVICE);
- if (v != null) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- long[] pattern = {0, 100, 100, 150};
- v.vibrate(android.os.VibrationEffect.createWaveform(pattern, -1));
- } else {
- long[] pattern = {0, 100, 100, 150};
- v.vibrate(pattern, -1);
- }
- }
-
- if (txtGuestArchLabel != null) {
- txtGuestArchLabel.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(ctx, R.color.status_success)));
- txtGuestArchLabel.setTextColor(ContextCompat.getColor(ctx, R.color.text_on_warning));
- }
-
- Snackbars.make(fragment.getView(), fragment.getString(R.string.sync_msg_arch_compatible)).show();
-
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- Context laterCtx = fragment.getContext();
- if (!fragment.isAdded() || laterCtx == null) return; // S8: fragment gone during the 1.5s delay
- if (txtGuestArchLabel != null) {
- txtGuestArchLabel.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(laterCtx, R.color.surface_section)));
- txtGuestArchLabel.setTextColor(ContextCompat.getColor(laterCtx, R.color.status_success));
- }
- onComplete.run();
- }, 1500);
- }
-
- public void updateArchLabelsVisibility() {
- boolean isShareMode = host.isShareMode();
- boolean isServerRunning = host.isServerRunning();
-
- if (isShareMode) {
- // In Send mode: if the server is running, the file is up. We hide the one below.
- // If the server is NOT running, we show the one below.
- if (txtGuestArchLabel != null) {
- txtGuestArchLabel.setVisibility(isServerRunning ? View.GONE : View.VISIBLE);
- }
- } else {
- // In Receive mode: We always show the one below.
- if (txtGuestArchLabel != null) {
- txtGuestArchLabel.setVisibility(View.VISIBLE);
- }
- }
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckHost.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckHost.java
deleted file mode 100644
index bb9b6a0a3..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckHost.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * ============================================================================
- * Name : ArchCheckHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between SyncFragment and ArchCheckController (ADFA-4506).
- * The arch-label visibility depends on the sync mode toggle and the
- * share/APK server flags, which stay owned by the Fragment (and,
- * later, by the Share/APK controllers), so they are read back here.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-public interface ArchCheckHost {
- /** True while the rsync daemon or the APK server is running. */
- boolean isServerRunning();
-
- /** True when the sync-mode toggle is on "Share" (vs "Receive"). */
- boolean isShareMode();
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveController.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveController.java
deleted file mode 100644
index 3264c0518..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveController.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * ============================================================================
- * Name : ReceiveController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : The Receive flow of the Sync tab, carved out of SyncFragment
- * (strangler-fig, ADFA-4506): scan -> probe -> dry-run -> confirm ->
- * transfer, plus cancel and progress rendering. The probe/dry-run and
- * transfer state already live in SyncStateViewModel /
- * SyncProgressRepository (ADFA-4492); this controller is the view glue
- * and delegates cross-cutting concerns (scanner, arch dialogs,
- * system-protection, mode toggle) to the Fragment via ReceiveHost.
- * No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.iiab.controller.ui.dialog.BrandDialog;
-import androidx.fragment.app.Fragment;
-
-import java.io.File;
-
-import org.iiab.controller.R;
-import org.iiab.controller.SyncHandshakeHelper;
-import org.iiab.controller.sync.domain.ShareConfig;
-import org.iiab.controller.sync.transport.TransportEngine;
-
-public final class ReceiveController {
-
- private final Fragment fragment;
- private final ReceiveHost host;
-
- // Borrowed collaborators (set in bind()).
- private SyncStateViewModel syncVm;
- private ShareConfig shareConfig;
-
- // Borrowed views (set in bind()).
- private Button btnScanQr, btnCancelTransfer;
- private LinearLayout containerProgress;
- private TextView txtTransferFilename, txtTransferSpeed, txtTransferEta;
- private ProgressBar progressBarTransfer;
-
- private long lastTransferSeq = -1L; // 3b-2: fire terminal dialog once
-
- public ReceiveController(Fragment fragment, ReceiveHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Borrow the ViewModel + config and wire the Receive views/listeners. Called from onCreateView(). */
- public void bind(View root, SyncStateViewModel syncVm, ShareConfig shareConfig) {
- this.syncVm = syncVm;
- this.shareConfig = shareConfig;
-
- btnScanQr = root.findViewById(R.id.btn_scan_qr);
- btnCancelTransfer = root.findViewById(R.id.btn_cancel_transfer);
- containerProgress = root.findViewById(R.id.container_progress);
- txtTransferFilename = root.findViewById(R.id.txt_transfer_filename);
- txtTransferSpeed = root.findViewById(R.id.txt_transfer_speed);
- txtTransferEta = root.findViewById(R.id.txt_transfer_eta);
- progressBarTransfer = root.findViewById(R.id.progress_bar_transfer);
-
- btnScanQr.setOnClickListener(v -> {
- if (!host.isSystemOptimizedForSync()) {
- host.showPhantomWarningDialog(this::startReceiveFlow);
- return;
- }
- startReceiveFlow();
- });
-
- btnCancelTransfer.setOnClickListener(v -> {
- syncVm.getTransport().stop();
- host.disableSystemProtection();
- syncVm.releaseNetwork(); // ADFA-4496
- SyncProgressRepository.get().postIdle();
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- });
- }
-
- /** Called from the Fragment's QR-scanner ActivityResult callback. */
- public void handleScannedData(String scannedJson) {
- SyncHandshakeHelper.SyncCredentials creds = SyncHandshakeHelper.parsePayload(scannedJson);
- if (creds == null) {
- Toast.makeText(fragment.getContext(), fragment.getString(R.string.sync_toast_invalid_qr), Toast.LENGTH_SHORT).show();
- return;
- }
-
- // --- ARCHITECTURE VALIDATION ---
- int hostBits = creds.archBits;
- int guestBits = host.getArchBits();
-
- if (hostBits != 0 && hostBits != guestBits) {
- if (hostBits == 64 && guestBits == 32) {
- host.showArchIncompatibilityDialog(fragment.getString(R.string.sync_error_arch_hardware_32));
- return;
- } else if (hostBits == 32 && guestBits == 64) {
- boolean hardwareSupports32 = false;
- for (String abi : android.os.Build.SUPPORTED_ABIS) {
- if (abi.contains("v7a") || (abi.contains("arm") && !abi.contains("64"))) {
- hardwareSupports32 = true;
- break;
- }
- }
-
- if (hardwareSupports32) {
- host.showArchIncompatibilityDialog(fragment.getString(R.string.sync_error_arch_fixable));
- } else {
- host.showArchIncompatibilityDialog(fragment.getString(R.string.sync_error_arch_strict_64));
- }
- return;
- }
- }
-
- // --- EVERYTHING IS OK: PROCEED TO DOWNLOAD ---
- host.showArchCompatibilitySuccess(() -> {
- if (!creds.hasRootfs) {
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.sync_dialog_empty_host_title))
- .setMessage(fragment.getString(R.string.sync_dialog_empty_host_msg))
- .setPositive(fragment.getString(R.string.sync_dialog_btn_try_anyway), () -> startProbe(creds))
- .setNegative(fragment.getString(R.string.cancel), null)
- .show();
- } else {
- startProbe(creds);
- }
- });
- }
-
- /**
- * ADFA-4492 step 4: kick the pre-transfer probe + dry-run, which run in the
- * Activity-scoped ViewModel and publish their phases to SyncProgressRepository. The
- * controller only shows the connecting UI; renderTransfer() reacts to CONNECTING/
- * CALCULATING/CONFIRM/ABORTED, so the probe survives a recreation (theme toggle).
- */
- private void startProbe(SyncHandshakeHelper.SyncCredentials creds) {
- btnScanQr.setVisibility(View.GONE);
- containerProgress.setVisibility(View.VISIBLE);
- progressBarTransfer.setIndeterminate(true);
- txtTransferFilename.setText(fragment.getString(R.string.sync_msg_connecting));
- syncVm.startProbe(fragment.requireContext().getApplicationContext(), shareConfig, creds);
- }
-
- private void startTransfer(SyncHandshakeHelper.SyncCredentials creds, File destDir) {
- host.enableSystemProtection();
- if (!destDir.exists()) destDir.mkdirs();
-
- // 3b-2: progress flows through SyncProgressRepository so the UI re-binds after a
- // recreation; the listener must NOT touch fragment views, and the transport uses
- // the application context (it lives in the Activity-scoped ViewModel).
- SyncProgressRepository.get().postTransferring(0, "", "", "RootFS");
-
- // ADFA-5160: measure the bar against the dry-run's bytes-to-transfer — the amount
- // rsync itself computed for THIS transfer (resume-aware). Not the QR size estimate:
- // that reflects the sender's initial install and can be stale. 0 (no dry-run) makes
- // the transport fall back to rsync's own percent rather than a value we can't trust.
- long expectedTotal = syncVm.getPendingBytes();
-
- syncVm.getTransport().startClient(fragment.requireContext().getApplicationContext(), shareConfig, creds.ip, creds.port, creds.user, creds.pass, destDir.getAbsolutePath(), expectedTotal, new TransportEngine.SyncListener() {
- @Override
- public void onProgress(int percentage, String speed, String eta, String currentFile) {
- SyncProgressRepository.get().postTransferring(percentage, speed, eta, currentFile);
- }
-
- @Override
- public void onComplete(String message) {
- SyncProgressRepository.get().postSuccess(message);
- }
-
- @Override
- public void onError(String error) {
- SyncProgressRepository.get().postFailed(error);
- }
- });
- }
-
- /** Renders the transfer state from SyncProgressRepository; re-binds after recreation (3b-2). */
- public void renderTransfer(SyncTransferState st) {
- if (st == null) return;
- switch (st.phase) {
- case CONNECTING:
- ensureReceiveModeForTransfer();
- progressBarTransfer.setIndeterminate(true);
- txtTransferFilename.setText(fragment.getString(R.string.sync_msg_connecting));
- break;
- case CALCULATING:
- ensureReceiveModeForTransfer();
- progressBarTransfer.setIndeterminate(true);
- txtTransferFilename.setText(fragment.getString(R.string.sync_msg_calculating));
- break;
- case CONFIRM:
- // Dry-run is done; the plan (creds/destDir) lives in the ViewModel and survives
- // recreation, so re-show the confirm dialog once per fragment instance.
- ensureReceiveModeForTransfer();
- progressBarTransfer.setIndeterminate(true);
- if (st.seq > lastTransferSeq) {
- lastTransferSeq = st.seq;
- if (fragment.getContext() != null) {
- new BrandDialog(fragment.requireContext())
- .setTitle(st.title)
- .setMessage(st.message)
- .setCancelable(false)
- .setPositive(fragment.getString(R.string.sync_btn_start_transfer), () -> {
- SyncHandshakeHelper.SyncCredentials creds = syncVm.getPendingCreds();
- File destDir = syncVm.getPendingDestDir();
- if (creds != null && destDir != null) {
- startTransfer(creds, destDir);
- } else {
- SyncProgressRepository.get().postIdle();
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- }
- })
- .setNegative(fragment.getString(R.string.cancel), () -> {
- syncVm.cancelProbe();
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- })
- .show();
- }
- }
- break;
- case ABORTED:
- if (st.seq > lastTransferSeq) {
- lastTransferSeq = st.seq;
- if (fragment.getContext() != null)
- new BrandDialog(fragment.requireContext())
- .setTitle(st.title)
- .setMessage(st.message)
- .setPositive(fragment.getString(R.string.adb_enforcer_btn_ok), null)
- .show();
- syncVm.releaseNetwork(); // ADFA-4496
- SyncProgressRepository.get().postIdle();
- }
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- break;
- case TRANSFERRING:
- ensureReceiveModeForTransfer();
- progressBarTransfer.setIndeterminate(false);
- progressBarTransfer.setProgress(st.percent);
- txtTransferSpeed.setText(st.speed);
- txtTransferEta.setText(fragment.getString(R.string.sync_transfer_eta, st.eta));
- if (!st.file.isEmpty()) {
- String displayFile = st.file.length() > 40 ? "..." + st.file.substring(st.file.length() - 40) : st.file;
- txtTransferFilename.setText(fragment.getString(R.string.sync_transfer_filename, displayFile));
- }
- break;
- case SUCCESS:
- if (st.seq > lastTransferSeq) {
- lastTransferSeq = st.seq;
- host.disableSystemProtection();
- if (fragment.getContext() != null)
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.sync_success_title))
- .setMessage(st.message)
- .setPositive(fragment.getString(R.string.adb_enforcer_btn_ok), null)
- .show();
- syncVm.releaseNetwork(); // ADFA-4496
- SyncProgressRepository.get().postIdle();
- }
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- break;
- case FAILED:
- if (st.seq > lastTransferSeq) {
- lastTransferSeq = st.seq;
- host.disableSystemProtection();
- if (fragment.getContext() != null)
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.sync_error_title))
- .setMessage(fragment.getString(R.string.sync_error_body, st.message))
- .setPositive(fragment.getString(R.string.adb_enforcer_btn_ok), null)
- .show();
- syncVm.releaseNetwork(); // ADFA-4496
- SyncProgressRepository.get().postIdle();
- }
- containerProgress.setVisibility(View.GONE);
- btnScanQr.setVisibility(View.VISIBLE);
- break;
- case IDLE:
- default:
- break;
- }
- }
-
- /** Forces the Share tab into receive mode so the transfer progress is visible after a
- * recreation (the mode toggle resets otherwise). 3b-2. */
- private void ensureReceiveModeForTransfer() {
- host.selectReceiveMode();
- containerProgress.setVisibility(View.VISIBLE);
- btnScanQr.setVisibility(View.GONE);
- }
-
- /** Extracted from the Scan-QR click so the pre-flight can run it after "continue". */
- public void startReceiveFlow() {
- // EX6: the "safe to receive now?" rule lives in the pure TransferGuard domain.
- boolean serverRunning = host.isServerAlive();
- if (!org.iiab.controller.sync.domain.TransferGuard.canReceive(serverRunning).allowed) {
- new BrandDialog(fragment.requireContext())
- .setTitle(fragment.getString(R.string.sync_dialog_server_running_title))
- .setMessage(fragment.getString(R.string.sync_error_stop_server_first))
- .setPositive(fragment.getString(R.string.adb_enforcer_btn_ok), null)
- .show();
- return;
- }
- host.launchQrScanner();
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveHost.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveHost.java
deleted file mode 100644
index 6f09dcf33..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveHost.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * ============================================================================
- * Name : ReceiveHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between SyncFragment and ReceiveController (ADFA-4506).
- * The receive flow (scan -> probe -> dry-run -> transfer) is owned
- * by the controller; the QR scanner (registerForActivityResult),
- * the mode toggle, the arch dialogs and the system-protection are
- * owned/shared by the Fragment and reached through this Host.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-public interface ReceiveHost {
- /** True when the embedded IIAB server is running (via the app-level ServerStateRepository). */
- boolean isServerAlive();
-
- /** ADFA-4496 pre-flight: true when the phantom-process monitor is NOT active. */
- boolean isSystemOptimizedForSync();
-
- /** Show the informed phantom-process warning; run onContinue if the user proceeds. */
- void showPhantomWarningDialog(Runnable onContinue);
-
- /** Start/stop the Watchdog foreground service that protects long transfers. */
- void enableSystemProtection();
- void disableSystemProtection();
-
- /** This device's architecture width in bits (for the QR arch check). */
- int getArchBits();
-
- /** Arch (in)compatibility feedback — delegated to ArchCheckController. */
- void showArchIncompatibilityDialog(String message);
- void showArchCompatibilitySuccess(Runnable onComplete);
-
- /** Launch the QR scanner (the ActivityResult launcher lives on the Fragment). */
- void launchQrScanner();
-
- /** Ensure the mode toggle is on "Receive" (so the transfer UI is visible). */
- void selectReceiveMode();
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareController.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareController.java
deleted file mode 100644
index 68f317f40..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareController.java
+++ /dev/null
@@ -1,410 +0,0 @@
-/*
- * ============================================================================
- * Name : ShareController.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : The Share area of the Sync tab, carved out of SyncFragment
- * (strangler-fig, ADFA-4506). Owns the rsync data daemon and the
- * APK-sharing server together, because they are mutually exclusive
- * and share one QR canvas, the network selector, the card ordering
- * and each other's button visibility. System-protection (Watchdog /
- * phantom pre-flight) is shared with the Receive flow and stays on
- * the Fragment, reached through ShareHost. No behaviour change.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-import android.graphics.Bitmap;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-
-import java.io.File;
-
-import org.iiab.controller.ApkServer;
-import org.iiab.controller.R;
-import org.iiab.controller.SyncHandshakeHelper;
-import org.iiab.controller.sync.domain.ApkShareName;
-import org.iiab.controller.sync.domain.ShareConfig;
-import org.iiab.controller.sync.transport.NetworkInterfaces;
-import org.iiab.controller.sync.transport.TransportEngine;
-import org.iiab.controller.ui.dialog.BrandDialog;
-import org.iiab.controller.util.AppExecutors;
-
-public final class ShareController {
-
- private static final String TAG = "IIAB-ShareController";
-
- private final Fragment fragment;
- private final ShareHost host;
-
- // Borrowed collaborators (set in bind()).
- private TransportEngine transport;
- private ShareConfig shareConfig;
-
- // Borrowed views (set in bind()).
- private LinearLayout containerShare;
- private Button btnStartServer;
- private Button btnShareApp;
- private ImageView imgQrCode;
- private TextView txtShareStatus;
- private TextView txtShareIp; // ADFA-4496: advertised IP + interface under the QR
- private LinearLayout qrDisplaySection;
- private View qrCardContainer;
- private RadioGroup rgNetworkSelector;
- private RadioButton rbNetWifi, rbNetHotspot;
- private LinearLayout cardShareSystem;
- private LinearLayout cardShareApk;
-
- // Owned state.
- private ApkServer apkServer;
- private String apkFileName; // K2Go--.apk, shared by the header and the QR URL (ADFA-4540)
- private boolean isDaemonRunning = false;
- private boolean isApkServerRunning = false;
- private String wifiIp = null;
- private String hotspotIp = null;
- private boolean showingWifi = true;
- private String tempPass;
- private boolean hostHasRootfs = true;
-
- public ShareController(Fragment fragment, ShareHost host) {
- this.fragment = fragment;
- this.host = host;
- }
-
- /** Borrow the transport + config and wire the Share views/listeners. Called from onCreateView(). */
- public void bind(View root, TransportEngine transport, ShareConfig shareConfig) {
- this.transport = transport;
- this.shareConfig = shareConfig;
-
- containerShare = root.findViewById(R.id.container_share);
- imgQrCode = root.findViewById(R.id.img_qr_code);
- txtShareStatus = root.findViewById(R.id.txt_share_status);
- btnStartServer = root.findViewById(R.id.btn_start_server);
- btnShareApp = root.findViewById(R.id.btn_share_app);
- txtShareIp = root.findViewById(R.id.txt_share_ip);
- qrDisplaySection = root.findViewById(R.id.qr_display_section);
- rgNetworkSelector = root.findViewById(R.id.rg_network_selector);
- rbNetWifi = root.findViewById(R.id.rb_net_wifi);
- rbNetHotspot = root.findViewById(R.id.rb_net_hotspot);
- cardShareSystem = root.findViewById(R.id.card_share_system);
- cardShareApk = root.findViewById(R.id.card_share_apk);
- qrCardContainer = root.findViewById(R.id.qr_card_container);
-
- // Network switch: crossfade the QR and reload it for the active server.
- rgNetworkSelector.setOnCheckedChangeListener((group, checkedId) -> {
- imgQrCode.animate().alpha(0f).setDuration(150).withEndAction(() -> {
- showingWifi = (checkedId == R.id.rb_net_wifi);
- rbNetWifi.setTextColor(showingWifi ? fragment.getResources().getColor(R.color.dash_text_primary) : fragment.getResources().getColor(R.color.dash_text_secondary));
- rbNetHotspot.setTextColor(!showingWifi ? fragment.getResources().getColor(R.color.dash_text_primary) : fragment.getResources().getColor(R.color.dash_text_secondary));
- if (isDaemonRunning) updateQrDisplayRsync();
- if (isApkServerRunning) updateQrDisplayApk();
- imgQrCode.animate().alpha(1f).setDuration(150).start();
- }).start();
- });
-
- // RSYNC SERVER LOGIC (data syncing) — informed pre-flight, then startShareFlow().
- btnStartServer.setOnClickListener(v -> {
- if (fragment.getActivity() == null) return;
- if (!host.isSystemOptimizedForSync()) {
- host.showPhantomWarningDialog(this::startShareFlow);
- return;
- }
- startShareFlow();
- });
-
- // APK SERVER LOGIC (app sharing / bootstrap).
- btnShareApp.setOnClickListener(v -> {
- if (isDaemonRunning) {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.sync_dialog_server_running_title)
- .setMessage(R.string.sync_error_stop_server_first)
- .setPositive(R.string.adb_enforcer_btn_ok, BrandDialog.Role.PRIMARY, null)
- .show();
- return;
- }
-
- if (!isApkServerRunning) {
- fetchNetworkInterfaces();
- if (wifiIp == null && hotspotIp == null) {
- Toast.makeText(fragment.getContext(), fragment.getString(R.string.sync_error_no_network), Toast.LENGTH_SHORT).show();
- return;
- }
- startApkServer();
- } else {
- stopApkServer();
- }
- });
- }
-
- /** True while the rsync daemon or the APK server is running. */
- public boolean isServerRunning() {
- return isDaemonRunning || isApkServerRunning;
- }
-
- /** Quiet teardown for onDestroyView (no UI touches); the shared transport is stopped by the Fragment. */
- public void stopApkServerQuietly() {
- if (apkServer != null) {
- apkServer.stop();
- apkServer = null;
- }
- isApkServerRunning = false;
- }
-
- private void fetchNetworkInterfaces() {
- // EX3: single source of LAN IP discovery (shared with QrActivity).
- NetworkInterfaces.LanIps ips = NetworkInterfaces.discover();
- wifiIp = ips.wifiIp;
- hotspotIp = ips.hotspotIp;
- }
-
- // --- RSYNC DAEMON METHODS ---
- private void startShareDaemon(File rootfsDir) {
- tempPass = SyncHandshakeHelper.generateSecurePassword();
- if (!rootfsDir.exists()) rootfsDir.mkdirs();
-
- // Start the rsync daemon off the main thread (file IO + ProcessBuilder.start
- // would otherwise risk an ANR on the UI thread); apply the result on the UI.
- final String shareDir = rootfsDir.getAbsolutePath();
- AppExecutors.get().io().execute(() -> {
- boolean started = transport.startServer(fragment.requireContext(), shareConfig, tempPass, shareDir);
- if (!fragment.isAdded() || fragment.getActivity() == null) return;
- fragment.requireActivity().runOnUiThread(() -> onShareDaemonResult(started));
- });
- }
-
- private void onShareDaemonResult(boolean started) {
- if (!fragment.isAdded()) return;
- if (started) {
- isDaemonRunning = true;
- host.enableSystemProtection();
- host.updateArchLabelsVisibility();
-
- qrDisplaySection.setVisibility(View.VISIBLE);
- qrCardContainer.setVisibility(View.VISIBLE);
- imgQrCode.setAlpha(1f);
- cardShareSystem.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.surface_active_success)));
-
- rgNetworkSelector.setVisibility((wifiIp != null && hotspotIp != null) ? View.VISIBLE : View.GONE);
- showingWifi = (wifiIp != null);
- updateQrDisplayRsync();
-
- btnStartServer.setText(fragment.getString(R.string.sync_btn_stop_server));
- btnStartServer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger))); // Red
- btnShareApp.setVisibility(View.GONE);
- } else {
- Toast.makeText(fragment.getContext(), fragment.getString(R.string.sync_error_daemon_failed), Toast.LENGTH_SHORT).show();
- }
- }
-
- /** ADFA-4496: show the IP (and which interface) the QR advertises, so a stale QR from a
- * previous network is obvious instead of looking like a transfer bug. */
- private void updateShareIpLabel(String ip) {
- if (txtShareIp == null) return;
- if (ip == null) {
- txtShareIp.setVisibility(View.GONE);
- return;
- }
- String iface = fragment.getString(showingWifi ? R.string.wifi : R.string.hotspot);
- txtShareIp.setText(iface + " " + ip);
- txtShareIp.setVisibility(View.VISIBLE);
- }
-
- private void updateQrDisplayRsync() {
- String currentIp = showingWifi ? wifiIp : hotspotIp;
- updateShareIpLabel(currentIp);
- String jsonPayload = SyncHandshakeHelper.createPayload(currentIp, shareConfig.rsyncPort, shareConfig.user, tempPass, hostHasRootfs, host.getArchBits());
- Bitmap qrBitmap = SyncHandshakeHelper.generateQrCode(jsonPayload, 500);
-
- if (qrBitmap != null) imgQrCode.setImageBitmap(qrBitmap);
-
- String baseText = showingWifi ? fragment.getString(R.string.sync_share_status_wifi) : fragment.getString(R.string.sync_share_status_hotspot);
- txtShareStatus.setText(baseText);
- txtShareStatus.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.text_secondary));
- }
-
- private void stopShareDaemon() {
- transport.stop();
- isDaemonRunning = false;
- host.disableSystemProtection();
- host.updateArchLabelsVisibility();
-
- qrDisplaySection.setVisibility(View.GONE);
- btnStartServer.setText(fragment.getString(R.string.sync_btn_start_server));
- cardShareSystem.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.surface_card)));
-
- btnStartServer.setText(fragment.getString(R.string.sync_btn_start_server));
- btnStartServer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_success))); // Green
- btnShareApp.setVisibility(View.VISIBLE);
- txtShareStatus.setText(fragment.getString(R.string.sync_share_status_off));
- txtShareStatus.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_warning)); // Orange
- }
-
- // --- APK SERVER METHODS ---
- /**
- * Arch label of the APK we are about to share, read from its own {@code lib//}
- * folders (the file that travels), not from the device. A universal build therefore
- * gets "universal" even on a single-ABI phone. Falls back to the device primary ABI
- * only if the APK can't be read. ADFA-4540.
- */
- private String apkArch(String apkPath) {
- java.util.Set abis = new java.util.LinkedHashSet<>();
- try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(apkPath)) {
- java.util.Enumeration extends java.util.zip.ZipEntry> entries = zip.entries();
- while (entries.hasMoreElements()) {
- String name = entries.nextElement().getName();
- if (name.startsWith("lib/")) {
- int slash = name.indexOf('/', 4);
- if (slash > 4) {
- abis.add(name.substring(4, slash));
- }
- }
- }
- } catch (Exception e) {
- Log.w(TAG, "Could not read ABIs from APK; falling back to device ABI", e);
- String[] dev = android.os.Build.SUPPORTED_ABIS;
- if (dev != null && dev.length > 0) {
- abis.add(dev[0]);
- }
- }
- return ApkShareName.archLabel(abis);
- }
-
- private void startApkServer() {
- try {
- String myApkPath = fragment.requireContext().getApplicationInfo().sourceDir;
-
- // ADFA-4540: stamp the download name with brand+version+arch so the receiver
- // knows exactly which build they got (replaces the ambiguous "-Latest").
- apkFileName = ApkShareName.fileName(org.iiab.controller.BuildConfig.VERSION_NAME, apkArch(myApkPath));
- apkServer = new ApkServer(shareConfig.apkPort, myApkPath, apkFileName);
- apkServer.start();
- isApkServerRunning = true;
-
- qrDisplaySection.setVisibility(View.VISIBLE);
- qrCardContainer.setVisibility(View.VISIBLE);
- updateCardOrder(true);
- imgQrCode.setAlpha(1f);
- cardShareApk.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.surface_active_info)));
-
- rgNetworkSelector.setVisibility((wifiIp != null && hotspotIp != null) ? View.VISIBLE : View.GONE);
- showingWifi = (wifiIp != null);
- updateQrDisplayApk();
-
- btnShareApp.setText(fragment.getString(R.string.sync_btn_stop_app));
- btnShareApp.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_danger))); // Red
- btnStartServer.setVisibility(View.GONE);
-
- } catch (Exception e) {
- Log.e(TAG, "Error starting APK Server", e);
- Toast.makeText(fragment.getContext(), fragment.getString(R.string.sync_error_daemon_failed), Toast.LENGTH_SHORT).show();
- }
- }
-
- private void updateQrDisplayApk() {
- String currentIp = showingWifi ? wifiIp : hotspotIp;
- updateShareIpLabel(currentIp);
- String downloadUrl = "http://" + currentIp + ":" + shareConfig.apkPort + "/" + apkFileName;
- Bitmap qrBitmap = SyncHandshakeHelper.generateQrCode(downloadUrl, 500);
-
- if (qrBitmap != null) imgQrCode.setImageBitmap(qrBitmap);
-
- String baseText = showingWifi ? fragment.getString(R.string.sync_app_status_wifi) : fragment.getString(R.string.sync_app_status_hotspot);
- txtShareStatus.setText(baseText);
- txtShareStatus.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.text_secondary));
- }
-
- private void stopApkServer() {
- if (apkServer != null) {
- apkServer.stop();
- apkServer = null;
- }
- isApkServerRunning = false;
- updateCardOrder(false);
-
- qrDisplaySection.setVisibility(View.GONE);
- btnShareApp.setText(fragment.getString(R.string.sync_btn_share_app));
- cardShareApk.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.surface_card)));
-
- btnShareApp.setText(fragment.getString(R.string.sync_btn_share_app));
- btnShareApp.setBackgroundTintList(android.content.res.ColorStateList.valueOf(ContextCompat.getColor(fragment.requireContext(), R.color.status_info))); // Blue
- btnStartServer.setVisibility(View.VISIBLE);
- txtShareStatus.setText(fragment.getString(R.string.sync_share_status_off));
- txtShareStatus.setTextColor(ContextCompat.getColor(fragment.requireContext(), R.color.status_warning)); // Orange
- }
-
- private void updateCardOrder(boolean isApkActive) {
- // Remove both cards from the parent container, then re-add in priority order.
- containerShare.removeView(cardShareSystem);
- containerShare.removeView(cardShareApk);
-
- if (isApkActive) {
- containerShare.addView(cardShareApk);
- containerShare.addView(cardShareSystem);
- } else {
- containerShare.addView(cardShareSystem);
- containerShare.addView(cardShareApk);
- }
- }
-
- /** Extracted from the Start-server click so the pre-flight can run it after "continue". */
- public void startShareFlow() {
- if (fragment.getActivity() == null) return;
-
- if (isApkServerRunning) {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.sync_dialog_server_running_title)
- .setMessage(R.string.sync_error_stop_apk_first)
- .setPositive(R.string.adb_enforcer_btn_ok, BrandDialog.Role.PRIMARY, null)
- .show();
- return;
- }
-
- if (host.isServerAlive()) {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.sync_dialog_server_running_title)
- .setMessage(R.string.sync_error_stop_server_first)
- .setPositive(R.string.adb_enforcer_btn_ok, BrandDialog.Role.PRIMARY, null)
- .show();
- return;
- }
-
- if (!isDaemonRunning) {
- fetchNetworkInterfaces();
- if (wifiIp == null && hotspotIp == null) {
- Toast.makeText(fragment.getContext(), fragment.getString(R.string.sync_error_no_network), Toast.LENGTH_SHORT).show();
- return;
- }
-
- File rootfsDir = new File(fragment.requireContext().getFilesDir(), "rootfs/installed-rootfs/iiab");
- hostHasRootfs = rootfsDir.exists() && rootfsDir.isDirectory();
-
- if (!hostHasRootfs) {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.sync_dialog_missing_env_title)
- .setMessage(R.string.sync_dialog_missing_env_msg)
- .setPositive(R.string.sync_dialog_btn_continue, BrandDialog.Role.PRIMARY, () -> startShareDaemon(rootfsDir))
- .setNegative(R.string.cancel, null)
- .show();
- } else {
- startShareDaemon(rootfsDir);
- }
- } else {
- new BrandDialog(fragment.requireContext())
- .setTitle(R.string.sync_dialog_stop_title)
- .setMessage(R.string.sync_dialog_stop_msg)
- .setPositive(R.string.sync_btn_stop_server, BrandDialog.Role.DESTRUCTIVE, () -> stopShareDaemon())
- .setNegative(R.string.cancel, null)
- .show();
- }
- }
-}
diff --git a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareHost.java b/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareHost.java
deleted file mode 100644
index afb2bb426..000000000
--- a/controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareHost.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * ============================================================================
- * Name : ShareHost.java
- * Author : AppDevForAll
- * Copyright : Copyright (c) 2026 AppDevForAll
- * Description : Seam between SyncFragment and ShareController (ADFA-4506).
- * The Share area (rsync daemon + APK server) is owned by the
- * controller; the system-protection (Watchdog / phantom pre-flight)
- * is shared with the Receive flow and stays on the Fragment, so the
- * controller reaches it through this Host. isServerAlive() bridges
- * the MainActivity coupling; the arch helpers delegate to
- * ArchCheckController.
- * ============================================================================
- */
-package org.iiab.controller.sync.presentation;
-
-public interface ShareHost {
- /** True when the embedded IIAB server is running (via the app-level ServerStateRepository). */
- boolean isServerAlive();
-
- /** ADFA-4496 pre-flight: true when the phantom-process monitor is NOT active. */
- boolean isSystemOptimizedForSync();
-
- /** Show the informed phantom-process warning; run onContinue if the user proceeds. */
- void showPhantomWarningDialog(Runnable onContinue);
-
- /** Start/stop the Watchdog foreground service that protects long transfers. */
- void enableSystemProtection();
- void disableSystemProtection();
-
- /** Re-evaluate the guest arch label visibility (delegates to ArchCheckController). */
- void updateArchLabelsVisibility();
-
- /** This device's architecture width in bits, for the rsync QR payload. */
- int getArchBits();
-}
diff --git a/controller/app/src/main/res/layout/fragment_dashboard.xml b/controller/app/src/main/res/layout/fragment_dashboard.xml
deleted file mode 100644
index 68c70ff1f..000000000
--- a/controller/app/src/main/res/layout/fragment_dashboard.xml
+++ /dev/null
@@ -1,492 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/controller/app/src/main/res/layout/fragment_deploy.xml b/controller/app/src/main/res/layout/fragment_deploy.xml
deleted file mode 100644
index 78f54ad3d..000000000
--- a/controller/app/src/main/res/layout/fragment_deploy.xml
+++ /dev/null
@@ -1,877 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/controller/app/src/main/res/layout/fragment_sync.xml b/controller/app/src/main/res/layout/fragment_sync.xml
deleted file mode 100644
index ac97457d9..000000000
--- a/controller/app/src/main/res/layout/fragment_sync.xml
+++ /dev/null
@@ -1,386 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/controller/app/src/main/res/layout/fragment_usage.xml b/controller/app/src/main/res/layout/fragment_usage.xml
deleted file mode 100644
index 917aeecfb..000000000
--- a/controller/app/src/main/res/layout/fragment_usage.xml
+++ /dev/null
@@ -1,472 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/controller/app/src/main/res/layout/main.xml b/controller/app/src/main/res/layout/main.xml
deleted file mode 100644
index 32b625dae..000000000
--- a/controller/app/src/main/res/layout/main.xml
+++ /dev/null
@@ -1,221 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/controller/app/src/main/res/layout/view_server_log.xml b/controller/app/src/main/res/layout/view_server_log.xml
deleted file mode 100644
index fe532f137..000000000
--- a/controller/app/src/main/res/layout/view_server_log.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/controller/app/src/main/res/values-ar/strings.xml b/controller/app/src/main/res/values-ar/strings.xml
index 32cb2184d..976ed9c50 100644
--- a/controller/app/src/main/res/values-ar/strings.xml
+++ b/controller/app/src/main/res/values-ar/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sالإعدادات
- التثبيت
- الحالة
- الاستخدام
- المشاركةالإعداد الأوليمرحبًا بك في معالج إعداد %1$s.\n\nلكي يعمل بشكل صحيح، نحتاج إلى الأذونات التالية:متابعة
diff --git a/controller/app/src/main/res/values-az/strings.xml b/controller/app/src/main/res/values-az/strings.xml
index ee888f40f..e002977fe 100644
--- a/controller/app/src/main/res/values-az/strings.xml
+++ b/controller/app/src/main/res/values-az/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sTƏNZİMLƏMƏLƏR
- Quraşdır
- Status
- İstifadə
- Paylaşİlkin quraşdırma%1$s quraşdırma sehrbazına xoş gəlmisiniz.\n\nDüzgün işləmək üçün aşağıdakı icazələr lazımdır:Davam et
diff --git a/controller/app/src/main/res/values-bg/strings.xml b/controller/app/src/main/res/values-bg/strings.xml
index 29566a1f8..30088b346 100644
--- a/controller/app/src/main/res/values-bg/strings.xml
+++ b/controller/app/src/main/res/values-bg/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sНАСТРОЙКИ
- Инсталиране
- Състояние
- Употреба
- СподелянеПървоначална настройкаДобре дошли в помощника за настройка на %1$s.\n\nЗа да работи правилно, се нуждаем от следните разрешения:Продължи
diff --git a/controller/app/src/main/res/values-bn/strings.xml b/controller/app/src/main/res/values-bn/strings.xml
index c6b3daadf..ab2f682ac 100644
--- a/controller/app/src/main/res/values-bn/strings.xml
+++ b/controller/app/src/main/res/values-bn/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sসেটিংস
- ইনস্টল
- অবস্থা
- ব্যবহার
- শেয়ারপ্রাথমিক সেটআপ%1$s সেটআপ উইজার্ডে স্বাগতম।\n\nসঠিকভাবে কাজ করার জন্য, আমাদের নিম্নলিখিত অনুমতিগুলো প্রয়োজন:চালিয়ে যান
diff --git a/controller/app/src/main/res/values-cs/strings.xml b/controller/app/src/main/res/values-cs/strings.xml
index a2cc78d0e..9d8d26840 100644
--- a/controller/app/src/main/res/values-cs/strings.xml
+++ b/controller/app/src/main/res/values-cs/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sNASTAVENÍ
- Instalace
- Stav
- Využití
- SdíletPočáteční nastaveníVítejte v průvodci nastavením %1$s.\n\nAby vše fungovalo správně, potřebujeme následující oprávnění:Pokračovat
diff --git a/controller/app/src/main/res/values-de/strings.xml b/controller/app/src/main/res/values-de/strings.xml
index 603896765..61d786f3d 100644
--- a/controller/app/src/main/res/values-de/strings.xml
+++ b/controller/app/src/main/res/values-de/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sEINSTELLUNGEN
- Installieren
- Status
- Nutzung
- TeilenErsteinrichtungWillkommen beim Einrichtungsassistenten von %1$s.\n\nDamit alles richtig funktioniert, benötigen wir folgende Berechtigungen:Weiter
diff --git a/controller/app/src/main/res/values-el/strings.xml b/controller/app/src/main/res/values-el/strings.xml
index 56e2639df..4da4cbf63 100644
--- a/controller/app/src/main/res/values-el/strings.xml
+++ b/controller/app/src/main/res/values-el/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sΡΥΘΜΙΣΕΙΣ
- Εγκατάσταση
- Κατάσταση
- Χρήση
- Κοινή χρήσηΑρχική ρύθμισηΚαλώς ήρθατε στον οδηγό ρύθμισης %1$s.\n\nΓια να λειτουργήσει σωστά, χρειαζόμαστε τις εξής άδειες:Συνέχεια
diff --git a/controller/app/src/main/res/values-es/strings.xml b/controller/app/src/main/res/values-es/strings.xml
index d176185f5..348639d9d 100644
--- a/controller/app/src/main/res/values-es/strings.xml
+++ b/controller/app/src/main/res/values-es/strings.xml
@@ -10,10 +10,6 @@
▶ %sAJUSTES
- Instalar
- Estado
- Uso
- EnviarConfiguración InicialBienvenido al asistente de configuración de %1$s.\n\nPara funcionar correctamente, necesitamos los siguientes permisos:
diff --git a/controller/app/src/main/res/values-fa/strings.xml b/controller/app/src/main/res/values-fa/strings.xml
index 10365d00c..98c8c4018 100644
--- a/controller/app/src/main/res/values-fa/strings.xml
+++ b/controller/app/src/main/res/values-fa/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sتنظیمات
- نصب
- وضعیت
- استفاده
- اشتراکگذاریراهاندازی اولیهبه دستیار راهاندازی %1$s خوش آمدید.\n\nبرای عملکرد صحیح، به مجوزهای زیر نیاز داریم:ادامه
diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml
index 1685702e2..f24f1a6d1 100644
--- a/controller/app/src/main/res/values-fr/strings.xml
+++ b/controller/app/src/main/res/values-fr/strings.xml
@@ -11,10 +11,6 @@
▶ %sPARAMÈTRES
- Installer
- Statut
- Utilisation
- PartagerConfiguration initialeBienvenue dans l\'assistant de configuration de %1$s.\n\nPour fonctionner correctement, nous avons besoin des autorisations suivantes :
diff --git a/controller/app/src/main/res/values-gu/strings.xml b/controller/app/src/main/res/values-gu/strings.xml
index 8ee3ce70e..9b6facec1 100644
--- a/controller/app/src/main/res/values-gu/strings.xml
+++ b/controller/app/src/main/res/values-gu/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sસેટિંગ્સ
- ઇન્સ્ટૉલ કરો
- સ્થિતિ
- વપરાશ
- શેર કરોપ્રારંભિક સેટઅપ%1$s સેટઅપ વિઝાર્ડમાં આપનું સ્વાગત છે.\n\nયોગ્ય રીતે કામ કરવા માટે, અમને નીચેની પરવાનગીઓ જોઈએ છે:ચાલુ રાખો
diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml
index 9f1acc664..07028ca21 100644
--- a/controller/app/src/main/res/values-hi/strings.xml
+++ b/controller/app/src/main/res/values-hi/strings.xml
@@ -11,10 +11,6 @@
▶ %sसेटिंग्स
- इंस्टॉल
- स्थिति
- उपयोग
- साझा करेंप्रारंभिक सेटअप%1$s सेटअप विज़ार्ड में आपका स्वागत है।\n\nठीक से काम करने के लिए, हमें निम्नलिखित अनुमतियों की आवश्यकता है:
diff --git a/controller/app/src/main/res/values-hu/strings.xml b/controller/app/src/main/res/values-hu/strings.xml
index eb749fe53..302ce05f7 100644
--- a/controller/app/src/main/res/values-hu/strings.xml
+++ b/controller/app/src/main/res/values-hu/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sBEÁLLÍTÁSOK
- Telepítés
- Állapot
- Használat
- MegosztásKezdeti beállításÜdvözli a(z) %1$s beállítási varázslója.\n\nA megfelelő működéshez a következő engedélyekre van szükségünk:Folytatás
diff --git a/controller/app/src/main/res/values-in/strings.xml b/controller/app/src/main/res/values-in/strings.xml
index d48b44969..400b52627 100644
--- a/controller/app/src/main/res/values-in/strings.xml
+++ b/controller/app/src/main/res/values-in/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sPENGATURAN
- Pasang
- Status
- Penggunaan
- BagikanPenyiapan AwalSelamat datang di wizard penyiapan %1$s.\n\nAgar dapat berfungsi dengan baik, kami memerlukan izin berikut:Lanjutkan
diff --git a/controller/app/src/main/res/values-it/strings.xml b/controller/app/src/main/res/values-it/strings.xml
index 4cdb47144..c82fb14c6 100644
--- a/controller/app/src/main/res/values-it/strings.xml
+++ b/controller/app/src/main/res/values-it/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sIMPOSTAZIONI
- Installa
- Stato
- Uso
- CondividiConfigurazione inizialeBenvenuto nella procedura guidata di configurazione di %1$s.\n\nPer funzionare correttamente, servono le seguenti autorizzazioni:Continua
diff --git a/controller/app/src/main/res/values-ja/strings.xml b/controller/app/src/main/res/values-ja/strings.xml
index c5d8446e6..4e9fd9813 100644
--- a/controller/app/src/main/res/values-ja/strings.xml
+++ b/controller/app/src/main/res/values-ja/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %s設定
- インストール
- ステータス
- 使用状況
- 共有初期設定%1$s のセットアップウィザードへようこそ。\n\n正しく動作させるには、次の権限が必要です:続行
diff --git a/controller/app/src/main/res/values-ko/strings.xml b/controller/app/src/main/res/values-ko/strings.xml
index f36436b5f..526dbe10d 100644
--- a/controller/app/src/main/res/values-ko/strings.xml
+++ b/controller/app/src/main/res/values-ko/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %s설정
- 설치
- 상태
- 사용
- 공유초기 설정%1$s 설정 마법사에 오신 것을 환영합니다.\n\n올바르게 작동하려면 다음 권한이 필요합니다:계속
diff --git a/controller/app/src/main/res/values-lt/strings.xml b/controller/app/src/main/res/values-lt/strings.xml
index 631858caa..e099db23b 100644
--- a/controller/app/src/main/res/values-lt/strings.xml
+++ b/controller/app/src/main/res/values-lt/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sNUSTATYMAI
- Diegti
- Būsena
- Naudojimas
- BendrintiPradinė sąrankaSveiki atvykę į %1$s sąrankos vediklį.\n\nKad viskas veiktų tinkamai, mums reikia šių leidimų:Tęsti
diff --git a/controller/app/src/main/res/values-nl/strings.xml b/controller/app/src/main/res/values-nl/strings.xml
index 89a2ff431..b954ef7de 100644
--- a/controller/app/src/main/res/values-nl/strings.xml
+++ b/controller/app/src/main/res/values-nl/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sINSTELLINGEN
- Installeren
- Status
- Gebruik
- DelenEerste installatieWelkom bij de installatiewizard van %1$s.\n\nOm goed te werken hebben we de volgende machtigingen nodig:Doorgaan
diff --git a/controller/app/src/main/res/values-no/strings.xml b/controller/app/src/main/res/values-no/strings.xml
index e1f3e019a..564e2c004 100644
--- a/controller/app/src/main/res/values-no/strings.xml
+++ b/controller/app/src/main/res/values-no/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sINNSTILLINGER
- Installer
- Status
- Bruk
- DelFørstegangsoppsettVelkommen til oppsettsveiviseren for %1$s.\n\nFor å fungere riktig trenger vi følgende tillatelser:Fortsett
diff --git a/controller/app/src/main/res/values-pl/strings.xml b/controller/app/src/main/res/values-pl/strings.xml
index bba0ff6cb..f914d1dd7 100644
--- a/controller/app/src/main/res/values-pl/strings.xml
+++ b/controller/app/src/main/res/values-pl/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sUSTAWIENIA
- Instalacja
- Stan
- Użycie
- UdostępnianieKonfiguracja początkowaWitamy w kreatorze konfiguracji %1$s.\n\nAby działać prawidłowo, potrzebujemy następujących uprawnień:Kontynuuj
diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml
index 0eea3a1fa..76705a681 100644
--- a/controller/app/src/main/res/values-pt/strings.xml
+++ b/controller/app/src/main/res/values-pt/strings.xml
@@ -11,10 +11,6 @@
▶ %sCONFIGURAÇÕES
- Instalar
- Status
- Uso
- CompartilharConfiguração InicialBem-vindo ao assistente de configuração do %1$s.\n\nPara funcionar corretamente, precisamos das seguintes permissões:
diff --git a/controller/app/src/main/res/values-ro/strings.xml b/controller/app/src/main/res/values-ro/strings.xml
index 0ed18e3ab..b06fef8c2 100644
--- a/controller/app/src/main/res/values-ro/strings.xml
+++ b/controller/app/src/main/res/values-ro/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sSETĂRI
- Instalare
- Stare
- Utilizare
- PartajareConfigurare inițialăBine ai venit la asistentul de configurare %1$s.\n\nPentru a funcționa corect, avem nevoie de următoarele permisiuni:Continuă
diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml
index 7571a8b0e..0fec24485 100644
--- a/controller/app/src/main/res/values-ru-rRU/strings.xml
+++ b/controller/app/src/main/res/values-ru-rRU/strings.xml
@@ -11,10 +11,6 @@
▶ %sНАСТРОЙКИ
- Установка
- Статус
- Использование
- ПоделитьсяНачальная настройкаДобро пожаловать в мастер настройки %1$s.\n\nДля правильной работы нам требуются следующие разрешения:
diff --git a/controller/app/src/main/res/values-sk/strings.xml b/controller/app/src/main/res/values-sk/strings.xml
index c1db7ab1e..fd806706c 100644
--- a/controller/app/src/main/res/values-sk/strings.xml
+++ b/controller/app/src/main/res/values-sk/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sNASTAVENIA
- Inštalácia
- Stav
- Využitie
- ZdieľaťÚvodné nastavenieVitajte v sprievodcovi nastavením %1$s.\n\nAby aplikácia fungovala správne, potrebujeme nasledujúce povolenia:Pokračovať
diff --git a/controller/app/src/main/res/values-sr/strings.xml b/controller/app/src/main/res/values-sr/strings.xml
index 1ee145e20..bf352e026 100644
--- a/controller/app/src/main/res/values-sr/strings.xml
+++ b/controller/app/src/main/res/values-sr/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sПОДЕШАВАЊА
- Инсталација
- Статус
- Употреба
- ДељењеПочетно подешавањеДобро дошли у чаробњак за подешавање %1$s.\n\nДа би радила исправно, потребне су нам следеће дозволе:Настави
diff --git a/controller/app/src/main/res/values-sw/strings.xml b/controller/app/src/main/res/values-sw/strings.xml
index 8527405bc..8dad5551e 100644
--- a/controller/app/src/main/res/values-sw/strings.xml
+++ b/controller/app/src/main/res/values-sw/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sMIPANGILIO
- Sakinisha
- Hali
- Matumizi
- ShirikiUsanidi wa AwaliKaribu kwenye mchawi wa usanidi wa %1$s.\n\nIli ifanye kazi vizuri, tunahitaji ruhusa zifuatazo:Endelea
diff --git a/controller/app/src/main/res/values-ta/strings.xml b/controller/app/src/main/res/values-ta/strings.xml
index b99c9e0b4..470262c49 100644
--- a/controller/app/src/main/res/values-ta/strings.xml
+++ b/controller/app/src/main/res/values-ta/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sஅமைப்புகள்
- நிறுவு
- நிலை
- பயன்பாடு
- பகிர்தொடக்க அமைப்பு%1$s அமைப்பு வழிகாட்டிக்கு வரவேற்கிறோம்.\n\nசரியாக இயங்க, எங்களுக்கு பின்வரும் அனுமதிகள் தேவை:தொடரவும்
diff --git a/controller/app/src/main/res/values-tr/strings.xml b/controller/app/src/main/res/values-tr/strings.xml
index 4a9d0e51f..6188ecffa 100644
--- a/controller/app/src/main/res/values-tr/strings.xml
+++ b/controller/app/src/main/res/values-tr/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sAYARLAR
- Yükle
- Durum
- Kullanım
- Paylaşİlk kurulum%1$s kurulum sihirbazına hoş geldiniz.\n\nDüzgün çalışması için aşağıdaki izinlere ihtiyacımız var:Devam
diff --git a/controller/app/src/main/res/values-uk/strings.xml b/controller/app/src/main/res/values-uk/strings.xml
index 98d368704..d040362bb 100644
--- a/controller/app/src/main/res/values-uk/strings.xml
+++ b/controller/app/src/main/res/values-uk/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sНАЛАШТУВАННЯ
- Встановлення
- Стан
- Використання
- ПоділитисяПочаткове налаштуванняЛаскаво просимо до майстра налаштування %1$s.\n\nЩоб працювати належним чином, нам потрібні такі дозволи:Продовжити
diff --git a/controller/app/src/main/res/values-vi/strings.xml b/controller/app/src/main/res/values-vi/strings.xml
index 56b8c07a9..3a15840a5 100644
--- a/controller/app/src/main/res/values-vi/strings.xml
+++ b/controller/app/src/main/res/values-vi/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sCÀI ĐẶT
- Cài đặt
- Trạng thái
- Sử dụng
- Chia sẻThiết lập ban đầuChào mừng đến với trình hướng dẫn thiết lập %1$s.\n\nĐể hoạt động đúng, chúng tôi cần các quyền sau:Tiếp tục
diff --git a/controller/app/src/main/res/values-yo/strings.xml b/controller/app/src/main/res/values-yo/strings.xml
index cd2990d00..6ca6ec9ca 100644
--- a/controller/app/src/main/res/values-yo/strings.xml
+++ b/controller/app/src/main/res/values-yo/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %sÀWỌN ÌṢÀGBÉKALẸ̀
- Fi sórí ẹ̀rọ
- Ipò
- Lílò
- PínÌṣàgbékalẹ̀ Àkọ́kọ́Káàbọ̀ sí olùtọ́ọ̀nà ìṣàgbékalẹ̀ %1$s.\n\nKí ó lè ṣiṣẹ́ dáadáa, a nílò àwọn ìgbàláàyè wọ̀nyí:Tẹ̀síwájú
diff --git a/controller/app/src/main/res/values-zh-rCN/strings.xml b/controller/app/src/main/res/values-zh-rCN/strings.xml
index 76481a43b..fbe70db24 100644
--- a/controller/app/src/main/res/values-zh-rCN/strings.xml
+++ b/controller/app/src/main/res/values-zh-rCN/strings.xml
@@ -9,10 +9,6 @@
▼ %s▶ %s设置
- 安装
- 状态
- 使用
- 共享初始设置欢迎使用 %1$s 设置向导。\n\n为了正常运行,我们需要以下权限:继续
diff --git a/controller/app/src/main/res/values/attrs.xml b/controller/app/src/main/res/values/attrs.xml
index d5c04fa3c..d27ea46ae 100644
--- a/controller/app/src/main/res/values/attrs.xml
+++ b/controller/app/src/main/res/values/attrs.xml
@@ -2,10 +2,6 @@
-
-
-
-
-
-
+
diff --git a/controller/app/src/main/res/values/ids.xml b/controller/app/src/main/res/values/ids.xml
new file mode 100644
index 000000000..f41bb84be
--- /dev/null
+++ b/controller/app/src/main/res/values/ids.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml
index e1da9172b..af1a8c4e4 100644
--- a/controller/app/src/main/res/values/strings.xml
+++ b/controller/app/src/main/res/values/strings.xml
@@ -11,10 +11,6 @@
▶ %sSETTINGS
- Install
- Status
- Usage
- ShareInitial Setup