From d7991c02d88a163b0b0ca3751a5f74c19990a1f9 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Wed, 19 Aug 2026 07:58:10 -0600 Subject: [PATCH 1/2] ADFA-5192 refactor(ui): delete the legacy tabbed UI (MainActivity, fragments, controllers) --- controller/app/src/main/AndroidManifest.xml | 19 +- .../iiab/controller/DashboardFragment.java | 744 ------------ .../org/iiab/controller/DashboardManager.java | 64 - .../org/iiab/controller/DeployFragment.java | 1071 ----------------- .../org/iiab/controller/MainActivity.java | 963 --------------- .../org/iiab/controller/MainPagerAdapter.java | 43 - .../controller/MultiResourceGaugeView.java | 179 --- .../org/iiab/controller/ProgressButton.java | 155 --- .../iiab/controller/ResourceGaugeView.java | 183 --- .../org/iiab/controller/ServerLogView.java | 267 ---- .../org/iiab/controller/SyncFragment.java | 261 ---- .../org/iiab/controller/UsageFragment.java | 603 ---------- .../backup/presentation/BackupController.java | 714 ----------- .../backup/presentation/BackupHost.java | 27 - .../presentation/AdbShareController.java | 516 -------- .../install/presentation/AdbShareHost.java | 16 - .../presentation/InstallController.java | 413 ------- .../install/presentation/InstallHost.java | 56 - .../presentation/PlannerController.java | 613 ---------- .../install/presentation/PlannerHost.java | 33 - .../presentation/ResetDeleteController.java | 185 --- .../install/presentation/ResetDeleteHost.java | 30 - .../controller/redesign/CloneFragment.java | 2 +- .../presentation/ArchCheckController.java | 158 --- .../sync/presentation/ArchCheckHost.java | 20 - .../sync/presentation/ReceiveController.java | 316 ----- .../sync/presentation/ReceiveHost.java | 41 - .../sync/presentation/ShareController.java | 410 ------- .../sync/presentation/ShareHost.java | 36 - .../main/res/layout/fragment_dashboard.xml | 492 -------- .../src/main/res/layout/fragment_deploy.xml | 877 -------------- .../app/src/main/res/layout/fragment_sync.xml | 386 ------ .../src/main/res/layout/fragment_usage.xml | 472 -------- controller/app/src/main/res/layout/main.xml | 221 ---- .../src/main/res/layout/view_server_log.xml | 52 - .../app/src/main/res/values-ar/strings.xml | 4 - .../app/src/main/res/values-az/strings.xml | 4 - .../app/src/main/res/values-bg/strings.xml | 4 - .../app/src/main/res/values-bn/strings.xml | 4 - .../app/src/main/res/values-cs/strings.xml | 4 - .../app/src/main/res/values-de/strings.xml | 4 - .../app/src/main/res/values-el/strings.xml | 4 - .../app/src/main/res/values-es/strings.xml | 4 - .../app/src/main/res/values-fa/strings.xml | 4 - .../app/src/main/res/values-fr/strings.xml | 4 - .../app/src/main/res/values-gu/strings.xml | 4 - .../app/src/main/res/values-hi/strings.xml | 4 - .../app/src/main/res/values-hu/strings.xml | 4 - .../app/src/main/res/values-in/strings.xml | 4 - .../app/src/main/res/values-it/strings.xml | 4 - .../app/src/main/res/values-ja/strings.xml | 4 - .../app/src/main/res/values-ko/strings.xml | 4 - .../app/src/main/res/values-lt/strings.xml | 4 - .../app/src/main/res/values-nl/strings.xml | 4 - .../app/src/main/res/values-no/strings.xml | 4 - .../app/src/main/res/values-pl/strings.xml | 4 - .../app/src/main/res/values-pt/strings.xml | 4 - .../app/src/main/res/values-ro/strings.xml | 4 - .../src/main/res/values-ru-rRU/strings.xml | 4 - .../app/src/main/res/values-sk/strings.xml | 4 - .../app/src/main/res/values-sr/strings.xml | 4 - .../app/src/main/res/values-sw/strings.xml | 4 - .../app/src/main/res/values-ta/strings.xml | 4 - .../app/src/main/res/values-tr/strings.xml | 4 - .../app/src/main/res/values-uk/strings.xml | 4 - .../app/src/main/res/values-vi/strings.xml | 4 - .../app/src/main/res/values-yo/strings.xml | 4 - .../src/main/res/values-zh-rCN/strings.xml | 4 - controller/app/src/main/res/values/attrs.xml | 8 +- .../app/src/main/res/values/strings.xml | 4 - 70 files changed, 9 insertions(+), 10773 deletions(-) delete mode 100644 controller/app/src/main/java/org/iiab/controller/DashboardFragment.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/DashboardManager.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/DeployFragment.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/MainActivity.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/MultiResourceGaugeView.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/ProgressButton.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/ResourceGaugeView.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/ServerLogView.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/SyncFragment.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/UsageFragment.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/backup/presentation/BackupHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/AdbShareHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/InstallController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/InstallHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/install/presentation/ResetDeleteHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ArchCheckHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ReceiveHost.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareController.java delete mode 100644 controller/app/src/main/java/org/iiab/controller/sync/presentation/ShareHost.java delete mode 100644 controller/app/src/main/res/layout/fragment_dashboard.xml delete mode 100644 controller/app/src/main/res/layout/fragment_deploy.xml delete mode 100644 controller/app/src/main/res/layout/fragment_sync.xml delete mode 100644 controller/app/src/main/res/layout/fragment_usage.xml delete mode 100644 controller/app/src/main/res/layout/main.xml delete mode 100644 controller/app/src/main/res/layout/view_server_log.xml 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/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 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -