1717import android .view .LayoutInflater ;
1818import android .view .View ;
1919import android .view .ViewGroup ;
20+ import android .widget .ImageView ;
21+ import android .widget .ScrollView ;
2022import android .widget .TextView ;
2123
2224import androidx .annotation .NonNull ;
2325import androidx .annotation .Nullable ;
2426import androidx .fragment .app .Fragment ;
2527
28+ import java .util .ArrayDeque ;
29+ import java .util .List ;
30+
31+ import org .iiab .controller .LogRepository ;
2632import org .iiab .controller .R ;
2733import org .iiab .controller .install .presentation .ModuleQueueRepository ;
2834import org .iiab .controller .install .presentation .ModuleQueueState ;
2935
3036public class MapsPreparingFragment extends Fragment {
3137
3238 private static final String ARG_LEVELS = "levels" ;
39+ private static final String ARG_FROM_INDEX = "fromIndex" ;
3340
3441 public static MapsPreparingFragment newInstance (String [] levels ) {
3542 MapsPreparingFragment f = new MapsPreparingFragment ();
@@ -39,48 +46,134 @@ public static MapsPreparingFragment newInstance(String[] levels) {
3946 return f ;
4047 }
4148
49+ /** ADFA-4901: open as the maps detail inside the Finishing-setup index — observe only, never
50+ * (re)start the install, and hide this fragment's own "Run in background" button (the index
51+ * host provides Back/Finish). Mirrors ZimPreparingFragment.newInstance(fromIndex). */
52+ public static MapsPreparingFragment newInstance (boolean fromIndex ) {
53+ MapsPreparingFragment f = new MapsPreparingFragment ();
54+ Bundle b = new Bundle ();
55+ b .putBoolean (ARG_FROM_INDEX , fromIndex );
56+ f .setArguments (b );
57+ return f ;
58+ }
59+
4260 private TextView status ;
61+ private boolean fromIndex = false ; // ADFA-4901: hosted by the Finishing-setup index (observe only)
4362 private boolean launched = false ; // ADFA-4900: guard against re-launching maps on view recreation
4463
64+ // ADFA-4901: collapsible live log (LogRepository) — the reusable terminal for proot module installs.
65+ private static final int MAX_LOG_LINES = 500 ; // bound the on-screen buffer (repo keeps more)
66+ private TextView logText , logLabel ;
67+ private ScrollView logScroll ;
68+ private ImageView logChevron ;
69+ private boolean logExpanded = false ;
70+ private boolean terminal = false ; // true once the queue reports DONE (stop live status override)
71+ private final ArrayDeque <String > logLines = new ArrayDeque <>();
72+ private LogRepository .Listener logListener ;
73+
4574 @ Nullable
4675 @ Override
4776 public View onCreateView (@ NonNull LayoutInflater inflater , @ Nullable ViewGroup container , @ Nullable Bundle s ) {
4877 View root = inflater .inflate (R .layout .fragment_k2go_maps_preparing , container , false );
4978
79+ fromIndex = getArguments () != null && getArguments ().getBoolean (ARG_FROM_INDEX , false );
80+
5081 status = root .findViewById (R .id .k2go_prep_status );
5182 status .setText (getString (R .string .k2go_maps_phase_prepared ));
5283
53- // Run in background -> back to the Get More hub (drops the whole Maps flow off the
54- // back stack), the build keeps going in the foreground service.
55- root .findViewById (R .id .k2go_prep_run_bg ).setOnClickListener (v -> {
56- if (getActivity () instanceof SetupLibraryActivity ) {
57- ((SetupLibraryActivity ) getActivity ()).backToGetMoreHub ();
58- }
59- });
84+ View runBg = root .findViewById (R .id .k2go_prep_run_bg );
85+ if (fromIndex ) {
86+ runBg .setVisibility (View .GONE ); // the index host provides Back/Finish; this card only observes
87+ } else {
88+ // Run in background -> back to the Get More hub (drops the whole Maps flow off the
89+ // back stack), the build keeps going in the foreground service.
90+ runBg .setOnClickListener (v -> {
91+ if (getActivity () instanceof SetupLibraryActivity ) {
92+ ((SetupLibraryActivity ) getActivity ()).backToGetMoreHub ();
93+ }
94+ });
95+ }
6096
61- // Start the real install only on first entry (not on a config-change recreation, and not
62- // if a maps job is already running/done from this session ).
97+ // Start the real install only on first entry (not on a config-change recreation, not when
98+ // hosted as the Finishing-setup detail, and not if a maps job is already running/done).
6399 String [] levels = getArguments () != null ? getArguments ().getStringArray (ARG_LEVELS ) : null ;
64- if (s == null && !launched
100+ if (s == null && !launched && ! fromIndex
65101 && getActivity () instanceof SetupLibraryActivity
66102 && !ModuleQueueRepository .get ().isRunning ()
67103 && ModuleQueueRepository .get ().current ().phase != ModuleQueueState .Phase .DONE ) {
68104 ((SetupLibraryActivity ) getActivity ()).startMapsInstall (levels );
69105 launched = true ;
70106 }
71107
72- // Follow the real queue state instead of a mock phase cycle.
108+ // Follow the real queue state. While RUNNING the status line is driven by the live log
109+ // (latest line); the phase text is only a fallback until the first line arrives. DONE is
110+ // terminal and always wins.
73111 ModuleQueueRepository .get ().state ().observe (getViewLifecycleOwner (), st -> {
74112 if (st == null ) return ;
75113 if (st .phase == ModuleQueueState .Phase .RUNNING ) {
76- status .setText (getString (R .string .k2go_maps_phase_building ));
114+ if ( logLines . isEmpty ()) status .setText (getString (R .string .k2go_maps_phase_building ));
77115 } else if (st .phase == ModuleQueueState .Phase .DONE ) {
116+ terminal = true ;
78117 status .setText (st .failedModules .isEmpty ()
79118 ? getString (R .string .k2go_maps_phase_ready )
80119 : getString (R .string .k2go_maps_phase_failed ));
81120 }
82121 });
83122
123+ // ADFA-4901: collapsible live log. The status line mirrors the latest line; expanding shows
124+ // the full terminal (LogRepository snapshot + live appends), auto-scrolled.
125+ logLabel = root .findViewById (R .id .k2go_prep_log_label );
126+ logChevron = root .findViewById (R .id .k2go_prep_log_chevron );
127+ logScroll = root .findViewById (R .id .k2go_prep_log_scroll );
128+ logText = root .findViewById (R .id .k2go_prep_log_text );
129+ root .findViewById (R .id .k2go_prep_log_toggle ).setOnClickListener (v -> toggleLog ());
130+
131+ logListener = new LogRepository .Listener () {
132+ @ Override public void onAppend (String line ) {
133+ if (!isAdded () || line == null ) return ;
134+ if (!terminal ) status .setText (line );
135+ if (logExpanded ) { pushLine (line ); renderLog (); }
136+ }
137+ @ Override public void onCleared () {
138+ logLines .clear ();
139+ if (logExpanded ) renderLog ();
140+ }
141+ };
142+ LogRepository .get ().addListener (logListener );
143+
84144 return root ;
85145 }
146+
147+ /** Expand/collapse the terminal. On expand, seed from the current LogRepository snapshot. */
148+ private void toggleLog () {
149+ logExpanded = !logExpanded ;
150+ logLabel .setText (getString (logExpanded ? R .string .k2go_maps_log_hide : R .string .k2go_maps_log_show ));
151+ logChevron .setRotation (logExpanded ? 90f : 0f );
152+ logScroll .setVisibility (logExpanded ? View .VISIBLE : View .GONE );
153+ if (logExpanded ) {
154+ logLines .clear ();
155+ List <String > snap = LogRepository .get ().snapshot ();
156+ int start = Math .max (0 , snap .size () - MAX_LOG_LINES );
157+ for (int i = start ; i < snap .size (); i ++) logLines .addLast (snap .get (i ));
158+ renderLog ();
159+ }
160+ }
161+
162+ private void pushLine (String line ) {
163+ logLines .addLast (line );
164+ while (logLines .size () > MAX_LOG_LINES ) logLines .removeFirst ();
165+ }
166+
167+ private void renderLog () {
168+ StringBuilder sb = new StringBuilder ();
169+ for (String l : logLines ) sb .append (l ).append ('\n' );
170+ logText .setText (sb .toString ());
171+ logScroll .post (() -> logScroll .fullScroll (View .FOCUS_DOWN ));
172+ }
173+
174+ @ Override
175+ public void onDestroyView () {
176+ if (logListener != null ) LogRepository .get ().removeListener (logListener );
177+ super .onDestroyView ();
178+ }
86179}
0 commit comments