3939import androidx .core .app .RemoteInput ;
4040import androidx .core .content .ContextCompat ;
4141import androidx .fragment .app .Fragment ;
42+ import androidx .lifecycle .ViewModelProvider ;
4243
4344import com .github .mikephil .charting .components .XAxis ;
4445import com .github .mikephil .charting .components .YAxis ;
4849import com .github .mikephil .charting .interfaces .datasets .ILineDataSet ;
4950import com .google .android .material .snackbar .Snackbar ;
5051
52+ import org .iiab .controller .rootfs .domain .RootfsAbi ;
53+ import org .iiab .controller .rootfs .domain .RootfsTier ;
54+ import org .iiab .controller .rootfs .presentation .RootfsUiState ;
55+ import org .iiab .controller .rootfs .presentation .RootfsViewModel ;
56+ import org .iiab .controller .rootfs .presentation .RootfsViewModelFactory ;
57+ import org .iiab .controller .util .ByteFormatter ;
58+
5159import org .json .JSONObject ;
5260
5361import java .io .BufferedReader ;
@@ -92,6 +100,7 @@ public class DeployFragment extends Fragment {
92100 // Planner UI
93101 private Button btnTierBasic , btnTierStandard , btnTierFull ;
94102 private TextView txtLegendIiab , txtLegendMaps , txtLegendKiwix , txtLegendFree ;
103+ private TextView txtOfflineEstimate ;
95104 private CheckBox chkCompanionData ;
96105 private MultiResourceGaugeView storageGauge ;
97106 private android .widget .ImageButton btnKiwixSettings ;
@@ -112,6 +121,10 @@ public class DeployFragment extends Fragment {
112121 private String overrideKiwixLang = null ;
113122 private String overrideKiwixVariant = null ;
114123 private InstallationPlanner .Tier selectedTier = null ;
124+ // Presentation-layer source of the OS rootfs size (live, with offline fallback).
125+ private RootfsViewModel rootfsViewModel ;
126+ // Last known connectivity, refreshed by checkInternetAccess() (every 3s via liveStatusRunnable).
127+ private volatile boolean hasInternet = true ;
115128
116129 // Native Engine Variables
117130 private static Aria2Manager aria2Manager ;
@@ -277,6 +290,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
277290 txtLegendMaps = view .findViewById (R .id .txt_legend_maps );
278291 txtLegendKiwix = view .findViewById (R .id .txt_legend_kiwix );
279292 txtLegendFree = view .findViewById (R .id .txt_legend_free );
293+ txtOfflineEstimate = view .findViewById (R .id .txt_offline_estimate );
280294
281295 // SAF Binding
282296 btnImportBackup = view .findViewById (R .id .btn_import_backup );
@@ -631,7 +645,7 @@ else if (isServerRunning)
631645
632646 } else {
633647 // FREE MODE: All off and ready to operate
634- if (selectedTier == null || !isStorageSafe ) btnFastInstall .setAlpha (0.4f );
648+ if (! hasInternet || selectedTier == null || !isStorageSafe ) btnFastInstall .setAlpha (0.4f );
635649 else btnFastInstall .setAlpha (1.0f );
636650
637651 btnFastDelete .setAlpha (1.0f );
@@ -642,7 +656,13 @@ else if (isServerRunning)
642656
643657 btnFastInstall .setEnabled (true );
644658 btnFastInstall .setTextSize (14f );
645- btnFastInstall .setText (isProotInstalled ? R .string .install_btn_reinstall : R .string .install_btn_install );
659+ if (!hasInternet ) {
660+ // Offline: downloading is impossible. Signal it on the button itself;
661+ // the click listener shows a snackbar instead of starting a failing download.
662+ btnFastInstall .setText (R .string .install_btn_no_connection );
663+ } else {
664+ btnFastInstall .setText (isProotInstalled ? R .string .install_btn_reinstall : R .string .install_btn_install );
665+ }
646666
647667 // Unlock checkboxes
648668 for (CheckBox cb : newInstallCheckboxes ) {
@@ -660,6 +680,13 @@ else if (isServerRunning)
660680 // =========================================================================================
661681
662682 private void setupPlannerListeners () {
683+ // Presentation layer: the projection UI consumes the OS rootfs size from
684+ // RootfsViewModel (live, with offline fallback) instead of having
685+ // InstallationPlanner resolve it. The observer completes each projection
686+ // once the size is resolved.
687+ rootfsViewModel = new ViewModelProvider (this , new RootfsViewModelFactory ()).get (RootfsViewModel .class );
688+ rootfsViewModel .state ().observe (getViewLifecycleOwner (), this ::onRootfsSizeResolved );
689+
663690 btnTierBasic .setAlpha (0.5f );
664691 btnTierBasic .setBackgroundTintList (ColorStateList .valueOf (Color .parseColor ("#008000" )));
665692 btnTierStandard .setAlpha (0.5f );
@@ -699,11 +726,41 @@ else if (v.getId() == R.id.btn_tier_standard)
699726 }
700727
701728 private void recalculateProjection () {
729+ InstallationPlanner .Tier evalTier = (selectedTier != null ) ? selectedTier : InstallationPlanner .Tier .BASIC ;
730+ // Ask the presentation layer for the OS rootfs size. onRootfsSizeResolved()
731+ // (registered as an observer in setupPlannerListeners) reacts and finishes
732+ // the projection with the resolved size.
733+ if (rootfsViewModel != null ) {
734+ // When we already know we're offline, skip the live fetch (avoids the ~6s
735+ // network timeout) and go straight to the hardcoded fallback size.
736+ rootfsViewModel .load (toRootfsTier (evalTier ), detectRootfsAbi (), hasInternet );
737+ }
738+ }
739+
740+ /**
741+ * Completes the storage projection once {@link RootfsViewModel} resolves the OS
742+ * size (live, or the offline fallback). The UI now consumes the size from the
743+ * presentation layer instead of having {@link InstallationPlanner} resolve it.
744+ */
745+ private void onRootfsSizeResolved (RootfsUiState rootfsState ) {
746+ if (!isAdded () || rootfsState == null ) return ;
747+ if (rootfsState .status == RootfsUiState .Status .LOADING ) return ;
748+
749+ final double osGiB = (rootfsState .rootfs != null )
750+ ? ByteFormatter .toGiB (rootfsState .rootfs .sizeBytes ())
751+ : 0.0 ;
752+
753+ // Show the "estimated (offline)" caption whenever the size is a fallback
754+ // (no live value), so the user knows the projection isn't server-confirmed.
755+ if (txtOfflineEstimate != null ) {
756+ txtOfflineEstimate .setVisibility (rootfsState .live ? View .GONE : View .VISIBLE );
757+ }
758+
702759 android .content .SharedPreferences prefs = requireContext ().getSharedPreferences (getString (R .string .pref_file_internal ), Context .MODE_PRIVATE );
703760 String targetLang = (overrideKiwixLang != null ) ? overrideKiwixLang : prefs .getString ("selected_lang_minimal" , "en" );
704761 InstallationPlanner .Tier evalTier = (selectedTier != null ) ? selectedTier : InstallationPlanner .Tier .BASIC ;
705762
706- InstallationPlanner .calculateProjectedSize (requireContext (), evalTier , chkCompanionData .isChecked (), targetLang , overrideKiwixVariant , new InstallationPlanner .PlanResultListener () {
763+ InstallationPlanner .calculateProjectedSize (requireContext (), evalTier , chkCompanionData .isChecked (), targetLang , overrideKiwixVariant , osGiB , new InstallationPlanner .PlanResultListener () {
707764 @ Override
708765 public void onCalculated (InstallationPlanner .StorageProjection projection ) {
709766 if (!isAdded ()) return ;
@@ -935,6 +992,13 @@ private void bindInstallButtonLogic(MainActivity mainAct, File debianRootfs, Fil
935992 return ;
936993 }
937994
995+ // 1b. No internet: a fresh install requires downloading the rootfs. Block it
996+ // up front (but still allow cancelling an in-progress download below).
997+ if (!hasInternet && !isDownloadingRootfs ) {
998+ Snackbar .make (v , R .string .install_msg_no_connection , Snackbar .LENGTH_LONG ).show ();
999+ return ;
1000+ }
1001+
9381002 // 2. HIGH PRIORITY: If this button is working, we allow cancel
9391003 if (isDownloadingRootfs ) {
9401004 new android .app .AlertDialog .Builder (requireContext ())
@@ -2661,6 +2725,25 @@ private String getTermuxArch() {
26612725 return "unknown" ;
26622726 }
26632727
2728+ /** Maps the legacy planner tier to the domain {@link RootfsTier}. */
2729+ private RootfsTier toRootfsTier (InstallationPlanner .Tier tier ) {
2730+ switch (tier ) {
2731+ case STANDARD :
2732+ return RootfsTier .STANDARD ;
2733+ case FULL :
2734+ return RootfsTier .FULL ;
2735+ case BASIC :
2736+ default :
2737+ return RootfsTier .BASIC ;
2738+ }
2739+ }
2740+
2741+ /** Detects the device ABI for rootfs selection, reusing {@link #getTermuxArch()}. */
2742+ private RootfsAbi detectRootfsAbi () {
2743+ String arch = getTermuxArch ();
2744+ return (arch != null && arch .contains ("64" )) ? RootfsAbi .ARM64_V8A : RootfsAbi .ARMEABI_V7A ;
2745+ }
2746+
26642747 public void openTermuxAppInfo () {
26652748 try {
26662749 android .content .Intent intent = new android .content .Intent (android .provider .Settings .ACTION_APPLICATION_DETAILS_SETTINGS );
@@ -2686,6 +2769,7 @@ private void checkInternetAccess() {
26862769 hasInternet = false ;
26872770 }
26882771 final boolean isOnline = hasInternet ;
2772+ DeployFragment .this .hasInternet = isOnline ;
26892773 if (isAdded () && getActivity () != null ) {
26902774 getActivity ().runOnUiThread (() -> {
26912775 if (ledInternet != null ) {
0 commit comments