From 1222be2450f99a371b31e316d472f53539dfe41e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Mon, 20 Jul 2026 21:36:27 -0600 Subject: [PATCH 01/11] =?UTF-8?q?ADFA-4785:=20Send=20the=20app=20=E2=80=94?= =?UTF-8?q?=20bootstrap=20K2Go=20onto=20a=20phone=20without=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Secondary 'Send the app first' entry on the Clone Send screen opens a sub-view that serves this app's APK over HTTP (reuses ApkServer) as a QR, with an Android share-sheet fallback (FileProvider) and a 'Next: copy the library' hand-off back to the normal rsync flow. The main Send flow is unchanged and stays the default. Per the mockup. Reuses ApkServer / ApkShareName. Needs a local build to verify. --- .../controller/redesign/CloneFragment.java | 87 +++++++++++++++++++ .../main/res/layout/fragment_k2go_clone.xml | 57 ++++++++++++ 2 files changed, 144 insertions(+) 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 8e5b08f1..463a305b 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 @@ -8,6 +8,7 @@ import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -30,12 +31,16 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import java.io.File; +import org.iiab.controller.ApkServer; +import org.iiab.controller.BuildConfig; import org.iiab.controller.R; import org.iiab.controller.SyncHandshakeHelper; import org.iiab.controller.WatchdogService; +import org.iiab.controller.sync.domain.ApkShareName; import org.iiab.controller.hotspot.LocalHotspotManager; import org.iiab.controller.sync.domain.ShareConfig; import org.iiab.controller.sync.presentation.SyncProgressRepository; @@ -72,6 +77,12 @@ private enum Stage { JOIN, START } private TextView sizeSys, sizeContent, sizeTotal; private boolean userStopped = false; // true after Stop, prevents auto-restart on the next render private boolean protectionOn = false; // ADFA-4782: foreground WatchdogService currently held + // ADFA-4785: "Send the app" sub-screen (bootstrap a phone with no K2Go) — serves the APK over HTTP. + private boolean sendApp = false; + private ApkServer apkServer; + private String apkFileName; + private LinearLayout sendAppEntry, sendAppView; + private ImageView sendAppQr; private ActivityResultLauncher locationPerm; @@ -174,6 +185,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c codetext = v.findViewById(R.id.k2go_clone_codetext); copyBtn = v.findViewById(R.id.k2go_clone_copy); shareBtn = v.findViewById(R.id.k2go_clone_share); + sendAppEntry = v.findViewById(R.id.k2go_sendapp_entry); + sendAppView = v.findViewById(R.id.k2go_sendapp_view); + sendAppQr = v.findViewById(R.id.k2go_sendapp_qr); + sendAppEntry.setOnClickListener(x -> { sendApp = true; render(); }); + v.findViewById(R.id.k2go_sendapp_back).setOnClickListener(x -> exitSendApp()); + v.findViewById(R.id.k2go_sendapp_next).setOnClickListener(x -> exitSendApp()); + v.findViewById(R.id.k2go_sendapp_share).setOnClickListener(x -> shareApkViaSheet()); syncVm = new ViewModelProvider(requireActivity()).get(SyncStateViewModel.class); transport = syncVm.getTransport(); @@ -334,12 +352,25 @@ private void render() { stop.setVisibility(View.GONE); footer.setVisibility(View.GONE); shareCard.setVisibility(View.GONE); + sendAppEntry.setVisibility(View.GONE); + sendAppView.setVisibility(View.GONE); receiveBox.setVisibility(View.VISIBLE); renderReceive(); syncProtection(); return; } receiveBox.setVisibility(View.GONE); + if (sendApp) { // ADFA-4785: "Send the app" sub-screen replaces the normal Send flow + netRow.setVisibility(View.GONE); steps.setVisibility(View.GONE); qr.setVisibility(View.GONE); + caption.setVisibility(View.GONE); subCaption.setVisibility(View.GONE); footer.setVisibility(View.GONE); + fallback.setVisibility(View.GONE); advance.setVisibility(View.GONE); stop.setVisibility(View.GONE); + shareCard.setVisibility(View.GONE); sendAppEntry.setVisibility(View.GONE); + sendAppView.setVisibility(View.VISIBLE); + renderSendApp(); + return; + } + sendAppView.setVisibility(View.GONE); + sendAppEntry.setVisibility(View.GONE); // renderHotspot's JOIN stage re-shows it netRow.setVisibility(View.VISIBLE); qr.setVisibility(View.VISIBLE); caption.setVisibility(View.VISIBLE); @@ -376,6 +407,7 @@ private void renderHotspot() { advance.setText("Show code 2 ›"); styleAdvance(true); footer.setText(""); + sendAppEntry.setVisibility(View.VISIBLE); // ADFA-4785: bootstrap entry on the first Send screen } else { advance.setText("‹ Back to step 1"); styleAdvance(false); @@ -430,6 +462,60 @@ private void renderStartState(String ip, boolean twoCode) { footer.setText("Stays on until you Stop."); } + // ------------------------------------------------------------ "Send the app" (ADFA-4785) + + private void renderSendApp() { + startApkServer(); + NetworkInterfaces.LanIps net = NetworkInterfaces.discover(); + String ip = (net.hotspotIp != null) ? net.hotspotIp : net.wifiIp; + if (ip == null || apkServer == null) { sendAppQr.setImageBitmap(null); return; } + String url = "http://" + ip + ":" + shareConfig.apkPort + "/" + apkFileName; + sendAppQr.setImageBitmap(SyncHandshakeHelper.generateQrCode(url, 500)); + } + + private void startApkServer() { + if (apkServer != null) return; + try { + String apkPath = requireContext().getApplicationInfo().sourceDir; + String arch = (archBits() == 64) ? "arm64-v8a" : "armeabi-v7a"; + apkFileName = ApkShareName.fileName(BuildConfig.VERSION_NAME, arch); + apkServer = new ApkServer(shareConfig.apkPort, apkPath, apkFileName); + apkServer.start(); + } catch (Exception e) { + Log.e("IIAB-Clone", "APK server failed to start", e); + Toast.makeText(requireContext(), "Couldn't start the app share", Toast.LENGTH_SHORT).show(); + } + } + + private void stopApkServer() { + if (apkServer != null) { + try { apkServer.stop(); } catch (Exception ignored) { } + apkServer = null; + } + } + + private void exitSendApp() { + stopApkServer(); + sendApp = false; + render(); + } + + /** Fallback for a phone that can't scan: hand the installed APK to the Android share sheet. */ + private void shareApkViaSheet() { + try { + File apk = new File(requireContext().getApplicationInfo().sourceDir); + Uri uri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".provider", apk); + Intent i = new Intent(Intent.ACTION_SEND) + .setType("application/vnd.android.package-archive") + .putExtra(Intent.EXTRA_STREAM, uri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(i, "Share K2Go app")); + } catch (Exception e) { + Log.e("IIAB-Clone", "APK share sheet failed", e); + Toast.makeText(requireContext(), "Couldn't open the share sheet", Toast.LENGTH_SHORT).show(); + } + } + private void showStopButton() { stop.setVisibility(View.VISIBLE); stop.setText("Stop sharing"); @@ -654,6 +740,7 @@ public void onDestroyView() { // keeps the (app-scoped) WatchdogService alive so leaving the tab doesn't cut the transfer. if (!SyncProgressRepository.get().isActive() && !daemonStarted) stopProtection(); if (!SyncProgressRepository.get().isActive()) syncVm.releaseNetwork(); + stopApkServer(); // ADFA-4785 } private void confirmStop() { diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index f839e157..4aa02a87 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -26,6 +26,18 @@ android:gravity="center" android:text="@string/k2go_wifi" android:clickable="true" android:focusable="true" /> + + + + + + + @@ -108,6 +120,51 @@ + + + + + + + + + + + + + + + + + + + + + + + From c15c815a3342bfbdf84620b53596ca637bca464d Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 14:36:18 -0600 Subject: [PATCH 02/11] ADFA-4785: intent fork (Send / Receive) as the Clone landing Replace the persistent top Send/Receive toggle with a first-screen intent fork: 'What do you want to do?' -> Send to another phone / Receive from another phone. Picking one enters that side; a '<- Back' header returns to the fork. Reuses the existing Side SEND/RECEIVE state and per-side rendering; the old toggle row is kept in the layout but hidden. No transport change. --- .../controller/redesign/CloneFragment.java | 46 ++++++++++++++++++- .../main/res/layout/fragment_k2go_clone.xml | 29 +++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) 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 d1694457..ed90c2d7 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 @@ -88,6 +88,10 @@ private enum Stage { JOIN, START } private ActivityResultLauncher locationPerm; private TextView tabSend, tabReceive, tabHotspot, tabWifi, caption, subCaption, advance, stop, footer; + // ADFA-4785: intent fork (Send / Receive) replaces the persistent top toggle. + private boolean atFork = true; + private LinearLayout forkBox, tabsRow; + private TextView cloneHdr, subtitleView, backHeader; // Receive side private SyncStateViewModel syncVm; private LinearLayout receiveBox, progressBox; @@ -241,10 +245,22 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c lastSeq = (cur != null) ? cur.seq : -1L; // only fire dialogs on NEW transitions SyncProgressRepository.get().state().observe(getViewLifecycleOwner(), this::onTransferState); - setSide(Side.SEND); + cloneHdr = v.findViewById(R.id.k2go_clone_hdr); + subtitleView = v.findViewById(R.id.k2go_clone_subtitle); + backHeader = v.findViewById(R.id.k2go_clone_back); + forkBox = v.findViewById(R.id.k2go_clone_fork); + tabsRow = v.findViewById(R.id.k2go_clone_tabs); + v.findViewById(R.id.k2go_clone_fork_send).setOnClickListener(x -> enterSide(Side.SEND)); + v.findViewById(R.id.k2go_clone_fork_receive).setOnClickListener(x -> enterSide(Side.RECEIVE)); + backHeader.setOnClickListener(x -> goToFork()); + render(); return v; } + private void enterSide(Side sd) { atFork = false; setSide(sd); } + + private void goToFork() { atFork = true; render(); } + private void setSide(Side sd) { side = sd; if (sd == Side.SEND) { setMode(Mode.HOTSPOT); return; } @@ -355,6 +371,34 @@ private void render() { paintTab(tabSend, side == Side.SEND); paintTab(tabReceive, side == Side.RECEIVE); + if (atFork) { + cloneHdr.setVisibility(View.VISIBLE); + subtitleView.setVisibility(View.VISIBLE); + forkBox.setVisibility(View.VISIBLE); + tabsRow.setVisibility(View.GONE); + backHeader.setVisibility(View.GONE); + netRow.setVisibility(View.GONE); + steps.setVisibility(View.GONE); + qr.setVisibility(View.GONE); + caption.setVisibility(View.GONE); + subCaption.setVisibility(View.GONE); + fallback.setVisibility(View.GONE); + advance.setVisibility(View.GONE); + stop.setVisibility(View.GONE); + footer.setVisibility(View.GONE); + shareCard.setVisibility(View.GONE); + sendAppEntry.setVisibility(View.GONE); + sendAppView.setVisibility(View.GONE); + receiveBox.setVisibility(View.GONE); + return; + } + forkBox.setVisibility(View.GONE); + tabsRow.setVisibility(View.GONE); + cloneHdr.setVisibility(View.GONE); + subtitleView.setVisibility(View.GONE); + backHeader.setVisibility(View.VISIBLE); + backHeader.setText(side == Side.RECEIVE ? "‹ Receive" : "‹ Send"); + if (side == Side.RECEIVE) { netRow.setVisibility(View.GONE); steps.setVisibility(View.GONE); diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index 6582620e..f455f3d9 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -5,12 +5,37 @@ - - + + + + + + + + + + + + + + + From 83dd44370953dea29c8a5e8c045d79d6e9301e96 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 14:53:35 -0600 Subject: [PATCH 03/11] ADFA-4785: Send 3-step spine (Connect / Get app / Copy), honest numbering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframe the Send badges as a 3-step spine over the existing pieces: - Connect (1) = the join stage; Get app (2) = the 'Send the app' sub-view; Copy (3) = the transfer/START stage. Steps never renumber. - Connect: 'Send the app first' card -> step 2; 'They already have K2Go ›' (advance) skips to Copy. Get app 'Next: copy the library' -> step 3 (done), 'Back' -> Connect. Copy 'Back to step 1' -> Connect. - Badge 2 shows filled/done when Get app was completed, neutral when skipped. Reuses renderSendApp / renderStartState; no transport change. (Explicit 'omitido' label on the skipped badge + the Wi-Fi Connect step are follow-ups.) --- .../controller/redesign/CloneFragment.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) 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 ed90c2d7..29083f2f 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 @@ -92,6 +92,7 @@ private enum Stage { JOIN, START } private boolean atFork = true; private LinearLayout forkBox, tabsRow; private TextView cloneHdr, subtitleView, backHeader; + private boolean getAppSkipped = false, getAppDone = false; // ADFA-4785: step-2 (Get app) disposition // Receive side private SyncStateViewModel syncVm; private LinearLayout receiveBox, progressBox; @@ -199,7 +200,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c sendAppQr = v.findViewById(R.id.k2go_sendapp_qr); sendAppEntry.setOnClickListener(x -> { sendApp = true; render(); }); v.findViewById(R.id.k2go_sendapp_back).setOnClickListener(x -> exitSendApp()); - v.findViewById(R.id.k2go_sendapp_next).setOnClickListener(x -> exitSendApp()); + v.findViewById(R.id.k2go_sendapp_next).setOnClickListener(x -> getAppDoneNext()); v.findViewById(R.id.k2go_sendapp_share).setOnClickListener(x -> shareApkViaSheet()); syncVm = new ViewModelProvider(requireActivity()).get(SyncStateViewModel.class); @@ -211,10 +212,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c tabHotspot.setOnClickListener(x -> setMode(Mode.HOTSPOT)); tabWifi.setOnClickListener(x -> setMode(Mode.WIFI)); advance.setOnClickListener(x -> { - if (mode == Mode.HOTSPOT) { - stage = (stage == Stage.JOIN) ? Stage.START : Stage.JOIN; - render(); - } + if (mode != Mode.HOTSPOT) return; + if (stage == Stage.JOIN) { getAppSkipped = true; stage = Stage.START; render(); } // skip Get app -> Copy + else { stage = Stage.JOIN; getAppSkipped = false; getAppDone = false; render(); } // back to Connect }); showcode.setOnClickListener(x -> { codeExpanded = !codeExpanded; @@ -257,7 +257,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return v; } - private void enterSide(Side sd) { atFork = false; setSide(sd); } + private void enterSide(Side sd) { atFork = false; getAppSkipped = false; getAppDone = false; sendApp = false; setSide(sd); } private void goToFork() { atFork = true; render(); } @@ -462,10 +462,10 @@ private void renderHotspot() { caption.setText("Scan code 1 to join"); subCaption.setText(R.string.k2go_just_scan); setFallback(new String[]{"Wi-Fi: " + ssid, "Password: " + pass}); - advance.setText("Show code 2 ›"); - styleAdvance(true); + advance.setText("They already have K2Go ›"); // ADFA-4785: skip Get app -> Copy + styleAdvance(false); footer.setText(""); - sendAppEntry.setVisibility(View.VISIBLE); // ADFA-4785: bootstrap entry on the first Send screen + sendAppEntry.setVisibility(View.VISIBLE); // "Send the app first" -> step 2 (Get app) } else { advance.setText("‹ Back to step 1"); styleAdvance(false); @@ -564,6 +564,14 @@ private void stopApkServer() { } } + private void getAppDoneNext() { // ADFA-4785: step 2 (Get app) done -> step 3 (Copy) + getAppDone = true; + stage = Stage.START; + stopApkServer(); + sendApp = false; + render(); + } + private void exitSendApp() { stopApkServer(); sendApp = false; @@ -892,14 +900,15 @@ private void setFallback(String[] values) { } // ---- step badges (same style as Connect): number kept, corner check when done ---- - private void buildSteps(boolean twoSteps) { + private void buildSteps(boolean twoSteps) { // ADFA-4785: 3-step spine (Connect / Get app / Copy) steps.removeAllViews(); - if (!twoSteps) { steps.setVisibility(View.GONE); return; } steps.setVisibility(View.VISIBLE); - boolean atStart = (stage == Stage.START); - steps.addView(badge("1", "Join", !atStart, atStart)); + int active = sendApp ? 2 : (mode == Mode.HOTSPOT && stage == Stage.JOIN ? 1 : 3); + steps.addView(badge("1", "Connect", active == 1, active > 1)); + steps.addView(arrow()); + steps.addView(badge("2", "Get app", active == 2, getAppDone)); steps.addView(arrow()); - steps.addView(badge("2", "Start", atStart && !daemonStarted, atStart && daemonStarted)); + steps.addView(badge("3", "Copy", active == 3, false)); } private View badge(String num, String label, boolean active, boolean done) { From 5907723e599191d93de6424d7f9abe47d62f2b6c Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:03:59 -0600 Subject: [PATCH 04/11] ADFA-4785: align Send step 1 (Connect) with the design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step title under the spine: 'Step 1 · Connect the other phone' (and 'Step 3 · Copy the library' on the copy step). - 'Next: get the app' is the primary button (advance) -> step 2. - 'Skip — they already have K2Go' as a small link at the bottom -> step 3. - Remove the 'Send the app first' card from the flow (it broke the sequence); Get app is now reached via the primary button. --- .../controller/redesign/CloneFragment.java | 25 +++++++++++++------ .../main/res/layout/fragment_k2go_clone.xml | 7 ++++++ 2 files changed, 25 insertions(+), 7 deletions(-) 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 29083f2f..414f8dbf 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 @@ -93,6 +93,7 @@ private enum Stage { JOIN, START } private LinearLayout forkBox, tabsRow; private TextView cloneHdr, subtitleView, backHeader; private boolean getAppSkipped = false, getAppDone = false; // ADFA-4785: step-2 (Get app) disposition + private TextView stepTitle, skipApp; // Receive side private SyncStateViewModel syncVm; private LinearLayout receiveBox, progressBox; @@ -213,8 +214,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c tabWifi.setOnClickListener(x -> setMode(Mode.WIFI)); advance.setOnClickListener(x -> { if (mode != Mode.HOTSPOT) return; - if (stage == Stage.JOIN) { getAppSkipped = true; stage = Stage.START; render(); } // skip Get app -> Copy - else { stage = Stage.JOIN; getAppSkipped = false; getAppDone = false; render(); } // back to Connect + if (stage == Stage.JOIN) { sendApp = true; render(); } // step 1 -> step 2 (Get app) + else { stage = Stage.JOIN; getAppDone = false; render(); } // step 3 -> back to step 1 }); showcode.setOnClickListener(x -> { codeExpanded = !codeExpanded; @@ -250,6 +251,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c backHeader = v.findViewById(R.id.k2go_clone_back); forkBox = v.findViewById(R.id.k2go_clone_fork); tabsRow = v.findViewById(R.id.k2go_clone_tabs); + stepTitle = v.findViewById(R.id.k2go_clone_steptitle); + skipApp = v.findViewById(R.id.k2go_clone_skipapp); + skipApp.setOnClickListener(x -> { getAppSkipped = true; stage = Stage.START; render(); }); v.findViewById(R.id.k2go_clone_fork_send).setOnClickListener(x -> enterSide(Side.SEND)); v.findViewById(R.id.k2go_clone_fork_receive).setOnClickListener(x -> enterSide(Side.RECEIVE)); backHeader.setOnClickListener(x -> goToFork()); @@ -368,6 +372,7 @@ private void stopProtection() { private void render() { if (!isAdded() || caption == null) return; if (showcode != null) { showcode.setVisibility(View.GONE); codeblock.setVisibility(View.GONE); } + if (stepTitle != null) { stepTitle.setVisibility(View.GONE); skipApp.setVisibility(View.GONE); } paintTab(tabSend, side == Side.SEND); paintTab(tabReceive, side == Side.RECEIVE); @@ -459,13 +464,17 @@ private void renderHotspot() { advance.setVisibility(View.VISIBLE); if (stage == Stage.JOIN) { setQr("WIFI:S:" + ssid + ";T:WPA;P:" + pass + ";;"); - caption.setText("Scan code 1 to join"); - subCaption.setText(R.string.k2go_just_scan); + stepTitle.setVisibility(View.VISIBLE); + stepTitle.setText("Step 1 · Connect the other phone"); + caption.setText("Point its camera here to join this phone."); + subCaption.setText(""); setFallback(new String[]{"Wi-Fi: " + ssid, "Password: " + pass}); - advance.setText("They already have K2Go ›"); // ADFA-4785: skip Get app -> Copy - styleAdvance(false); + advance.setText("Next: get the app"); + styleAdvance(true); footer.setText(""); - sendAppEntry.setVisibility(View.VISIBLE); // "Send the app first" -> step 2 (Get app) + sendAppEntry.setVisibility(View.GONE); + skipApp.setVisibility(View.VISIBLE); + skipApp.setText("Skip — they already have K2Go ›"); } else { advance.setText("‹ Back to step 1"); styleAdvance(false); @@ -485,6 +494,8 @@ private void renderWifi() { /** Step-2 state: starting -> stopped (Start sharing) -> running (QR + Stop sharing). */ private void renderStartState(String ip, boolean twoCode) { + stepTitle.setVisibility(View.VISIBLE); + stepTitle.setText("Step 3 · Copy the library"); if (!daemonStarted && !daemonStarting && !shareAnyway && !rootfsPresent()) { // ADFA-4786 qr.setImageBitmap(null); caption.setText("Nothing to share yet"); diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index f455f3d9..8a1c9b85 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -66,6 +66,9 @@ + + @@ -145,6 +148,10 @@ + + From b97bb551a287c78659f4387f25080a15a734bdae Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:10:08 -0600 Subject: [PATCH 05/11] ADFA-4785: align Send step 2 (Get app) with the spine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 now shows the shared spine + 'Step 2 · Get the app' title like steps 1 and 3, instead of its own 'Send the app' header. Dropped the redundant sub-view title/subtitle; back relabeled to '< Back' (-> Connect); primary button 'Installed? Copy the library' (-> Copy). Step 3 already carries its 'Step 3 · Copy the library' title. --- .../java/org/iiab/controller/redesign/CloneFragment.java | 6 ++++-- .../app/src/main/res/layout/fragment_k2go_clone.xml | 8 ++------ 2 files changed, 6 insertions(+), 8 deletions(-) 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 414f8dbf..afeee617 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 @@ -423,11 +423,13 @@ private void render() { return; } receiveBox.setVisibility(View.GONE); - if (sendApp) { // ADFA-4785: "Send the app" sub-screen replaces the normal Send flow - netRow.setVisibility(View.GONE); steps.setVisibility(View.GONE); qr.setVisibility(View.GONE); + if (sendApp) { // ADFA-4785: step 2 (Get app) — spine + step title, then the sub-view + netRow.setVisibility(View.GONE); qr.setVisibility(View.GONE); caption.setVisibility(View.GONE); subCaption.setVisibility(View.GONE); footer.setVisibility(View.GONE); fallback.setVisibility(View.GONE); advance.setVisibility(View.GONE); stop.setVisibility(View.GONE); shareCard.setVisibility(View.GONE); sendAppEntry.setVisibility(View.GONE); + steps.setVisibility(View.VISIBLE); buildSteps(true); + stepTitle.setVisibility(View.VISIBLE); stepTitle.setText("Step 2 · Get the app"); sendAppView.setVisibility(View.VISIBLE); renderSendApp(); return; diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index 8a1c9b85..97557481 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -156,11 +156,7 @@ - - + android:padding="6dp" android:text="‹ Back" android:textColor="@color/k2go_teal" android:textAppearance="?attr/textAppearanceBodyLarge" android:clickable="true" android:focusable="true" /> From 967a360a89fbced8ea560f966a5d8d138635d38c Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:16:55 -0600 Subject: [PATCH 06/11] ADFA-4785: fix step order (Join -> Get app -> Copy) in both modes; rename Connect->Join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Both Hotspot and Wi-Fi now start at step 1 (Join); setMode no longer sends Wi-Fi straight to Copy. - 'Next: get the app' advances to step 2 in BOTH modes (removed the hotspot-only guard on advance); step 3 (Copy) is reached only via 'Installed? Copy the library' or 'Skip - they already have K2Go'. - Wi-Fi now has a real step 1 (Join): instructions + Next + skip (no QR; the sender can't mint a Wi-Fi QR — open-settings helper is a follow-up). - Renamed the step 'Connect' -> 'Join' (badge + title) to avoid confusion with the Connect tab. Spine active step is now mode-agnostic. --- .../controller/redesign/CloneFragment.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) 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 afeee617..f892d5a7 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 @@ -213,7 +213,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c tabHotspot.setOnClickListener(x -> setMode(Mode.HOTSPOT)); tabWifi.setOnClickListener(x -> setMode(Mode.WIFI)); advance.setOnClickListener(x -> { - if (mode != Mode.HOTSPOT) return; if (stage == Stage.JOIN) { sendApp = true; render(); } // step 1 -> step 2 (Get app) else { stage = Stage.JOIN; getAppDone = false; render(); } // step 3 -> back to step 1 }); @@ -275,7 +274,7 @@ private void setSide(Side sd) { private void setMode(Mode m) { mode = m; - stage = (m == Mode.HOTSPOT) ? Stage.JOIN : Stage.START; + stage = Stage.JOIN; // ADFA-4785: both modes start at step 1 (Join), not straight to Copy if (m == Mode.HOTSPOT) ensureHotspot(); render(); } @@ -467,7 +466,7 @@ private void renderHotspot() { if (stage == Stage.JOIN) { setQr("WIFI:S:" + ssid + ";T:WPA;P:" + pass + ";;"); stepTitle.setVisibility(View.VISIBLE); - stepTitle.setText("Step 1 · Connect the other phone"); + stepTitle.setText("Step 1 · Join the other phone"); caption.setText("Point its camera here to join this phone."); subCaption.setText(""); setFallback(new String[]{"Wi-Fi: " + ssid, "Password: " + pass}); @@ -489,9 +488,26 @@ private void renderWifi() { String ip = NetworkInterfaces.discover().wifiIp; if (ip == null) { buildSteps(false); advance.setVisibility(View.GONE); simpleState("Not on a Wi-Fi network", "Join a Wi-Fi, or use Hotspot."); return; } buildSteps(false); - advance.setVisibility(View.GONE); - ensureDaemon(ip); - renderStartState(ip, false); + if (stage == Stage.JOIN) { + qr.setVisibility(View.GONE); + stepTitle.setVisibility(View.VISIBLE); + stepTitle.setText("Step 1 · Join the other phone"); + caption.setText("Get the other phone onto this Wi-Fi."); + subCaption.setText("Share this Wi-Fi from Settings, or have them join it, then continue."); + setFallback(null); + footer.setText(""); + advance.setVisibility(View.VISIBLE); + advance.setText("Next: get the app"); + styleAdvance(true); + skipApp.setVisibility(View.VISIBLE); + skipApp.setText("Skip — they already have K2Go ›"); + } else { + advance.setVisibility(View.VISIBLE); + advance.setText("‹ Back to step 1"); + styleAdvance(false); + ensureDaemon(ip); + renderStartState(ip, false); + } } /** Step-2 state: starting -> stopped (Start sharing) -> running (QR + Stop sharing). */ @@ -916,8 +932,8 @@ private void setFallback(String[] values) { private void buildSteps(boolean twoSteps) { // ADFA-4785: 3-step spine (Connect / Get app / Copy) steps.removeAllViews(); steps.setVisibility(View.VISIBLE); - int active = sendApp ? 2 : (mode == Mode.HOTSPOT && stage == Stage.JOIN ? 1 : 3); - steps.addView(badge("1", "Connect", active == 1, active > 1)); + int active = sendApp ? 2 : (stage == Stage.JOIN ? 1 : 3); + steps.addView(badge("1", "Join", active == 1, active > 1)); steps.addView(arrow()); steps.addView(badge("2", "Get app", active == 2, getAppDone)); steps.addView(arrow()); From 60a70dd7cb9ed9b456ce89057b19b6868b2a4438 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:27:34 -0600 Subject: [PATCH 07/11] ADFA-4785: Wi-Fi 'Share Wi-Fi via settings' secondary + make the step-1 skip prominent - Wi-Fi step 1 (Join) gains a secondary 'Share Wi-Fi via Android settings' button (opens Settings so the host can share the network QR); primary stays 'Next: get the app'. - The 'Skip - they already have K2Go' link is now bold, right-aligned and sits directly under the 'Next: get the app' button (was a small centered note), so it reads as a real alternative that jumps step 1 -> step 3. --- .../java/org/iiab/controller/redesign/CloneFragment.java | 5 +++++ controller/app/src/main/res/layout/fragment_k2go_clone.xml | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) 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 f892d5a7..a7b1012c 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 @@ -499,6 +499,11 @@ private void renderWifi() { advance.setVisibility(View.VISIBLE); advance.setText("Next: get the app"); styleAdvance(true); + stop.setVisibility(View.VISIBLE); + stop.setText("Share Wi-Fi via Android settings"); + stop.setBackgroundResource(R.drawable.k2go_getmore_bg); + stop.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal)); + stop.setOnClickListener(x -> openWifiSettings()); skipApp.setVisibility(View.VISIBLE); skipApp.setText("Skip — they already have K2Go ›"); } else { diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index 97557481..43240ddc 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -140,6 +140,10 @@ android:layout_marginTop="16dp" android:padding="16dp" android:gravity="center" android:textAppearance="?attr/textAppearanceTitleLarge" android:clickable="true" android:focusable="true" android:visibility="gone" /> + + - Date: Tue, 21 Jul 2026 15:33:31 -0600 Subject: [PATCH 08/11] ADFA-4785: switching Hotspot/Wi-Fi keeps the current step (no jump to step 1) setMode reset stage to JOIN, so toggling Hotspot<->Wi-Fi threw the user back to step 1 even from Copy. Decouple mode from step: setMode only changes the network and re-renders the current step; entering Send (setSide) is what starts at step 1. Switching network at Copy now just refreshes the QR with the new IP. --- .../java/org/iiab/controller/redesign/CloneFragment.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 a7b1012c..e764c786 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 @@ -266,7 +266,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c private void setSide(Side sd) { side = sd; - if (sd == Side.SEND) { setMode(Mode.HOTSPOT); return; } + if (sd == Side.SEND) { stage = Stage.JOIN; setMode(Mode.HOTSPOT); return; } // ADFA-4785: enter Send at step 1 rStage = RStage.JOIN; pasteExpanded = false; incompatHostBits = -1; incompatWhyOpen = false; incompatTechOpen = false; // ADFA-4784: fresh on entry render(); @@ -274,9 +274,8 @@ private void setSide(Side sd) { private void setMode(Mode m) { mode = m; - stage = Stage.JOIN; // ADFA-4785: both modes start at step 1 (Join), not straight to Copy if (m == Mode.HOTSPOT) ensureHotspot(); - render(); + render(); // ADFA-4785: keep the current step; switching Hotspot/Wi-Fi no longer resets to step 1 } private void ensureHotspot() { From 4d020a55665e45443af247957fac1faf77ec5e60 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:38:51 -0600 Subject: [PATCH 09/11] =?UTF-8?q?ADFA-4785:=20Wi-Fi=20step=201=20=E2=80=94?= =?UTF-8?q?=20'Share=20Wi-Fi=20via=20settings'=20sits=20above=20the=20prim?= =?UTF-8?q?ary=20'Next'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the share action its own button placed above 'Next: get the app' (it's the immediate thing to do on arriving at the Wi-Fi tab), instead of reusing the Stop button below. Secondary style; 'Next' stays the primary, and the bold skip jumps straight to Copy. --- .../org/iiab/controller/redesign/CloneFragment.java | 12 +++++------- .../app/src/main/res/layout/fragment_k2go_clone.xml | 7 ++++++- 2 files changed, 11 insertions(+), 8 deletions(-) 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 e764c786..f2bbce66 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 @@ -93,7 +93,7 @@ private enum Stage { JOIN, START } private LinearLayout forkBox, tabsRow; private TextView cloneHdr, subtitleView, backHeader; private boolean getAppSkipped = false, getAppDone = false; // ADFA-4785: step-2 (Get app) disposition - private TextView stepTitle, skipApp; + private TextView stepTitle, skipApp, shareWifi; // Receive side private SyncStateViewModel syncVm; private LinearLayout receiveBox, progressBox; @@ -253,6 +253,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c stepTitle = v.findViewById(R.id.k2go_clone_steptitle); skipApp = v.findViewById(R.id.k2go_clone_skipapp); skipApp.setOnClickListener(x -> { getAppSkipped = true; stage = Stage.START; render(); }); + shareWifi = v.findViewById(R.id.k2go_clone_sharewifi); + shareWifi.setOnClickListener(x -> openWifiSettings()); v.findViewById(R.id.k2go_clone_fork_send).setOnClickListener(x -> enterSide(Side.SEND)); v.findViewById(R.id.k2go_clone_fork_receive).setOnClickListener(x -> enterSide(Side.RECEIVE)); backHeader.setOnClickListener(x -> goToFork()); @@ -370,7 +372,7 @@ private void stopProtection() { private void render() { if (!isAdded() || caption == null) return; if (showcode != null) { showcode.setVisibility(View.GONE); codeblock.setVisibility(View.GONE); } - if (stepTitle != null) { stepTitle.setVisibility(View.GONE); skipApp.setVisibility(View.GONE); } + if (stepTitle != null) { stepTitle.setVisibility(View.GONE); skipApp.setVisibility(View.GONE); shareWifi.setVisibility(View.GONE); } paintTab(tabSend, side == Side.SEND); paintTab(tabReceive, side == Side.RECEIVE); @@ -496,13 +498,9 @@ private void renderWifi() { setFallback(null); footer.setText(""); advance.setVisibility(View.VISIBLE); + shareWifi.setVisibility(View.VISIBLE); advance.setText("Next: get the app"); styleAdvance(true); - stop.setVisibility(View.VISIBLE); - stop.setText("Share Wi-Fi via Android settings"); - stop.setBackgroundResource(R.drawable.k2go_getmore_bg); - stop.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal)); - stop.setOnClickListener(x -> openWifiSettings()); skipApp.setVisibility(View.VISIBLE); skipApp.setText("Skip — they already have K2Go ›"); } else { diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml index 43240ddc..2eb93816 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -136,8 +136,13 @@ + + Date: Tue, 21 Jul 2026 15:43:13 -0600 Subject: [PATCH 10/11] ADFA-4785: step 2 (Get app) follows the chosen network for the APK QR Step 2 kept using the hotspot IP regardless of the selected network. Show the Hotspot/Wi-Fi selector on step 2 too, and build the APK download QR from the IP of the chosen mode (hotspot vs Wi-Fi); switching the selector refreshes the QR. --- .../java/org/iiab/controller/redesign/CloneFragment.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 f2bbce66..55f19064 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 @@ -423,8 +423,10 @@ private void render() { return; } receiveBox.setVisibility(View.GONE); - if (sendApp) { // ADFA-4785: step 2 (Get app) — spine + step title, then the sub-view - netRow.setVisibility(View.GONE); qr.setVisibility(View.GONE); + if (sendApp) { // ADFA-4785: step 2 (Get app) — spine + step title + network selector, then the sub-view + netRow.setVisibility(View.VISIBLE); qr.setVisibility(View.GONE); + paintTab(tabHotspot, mode == Mode.HOTSPOT); + paintTab(tabWifi, mode == Mode.WIFI); caption.setVisibility(View.GONE); subCaption.setVisibility(View.GONE); footer.setVisibility(View.GONE); fallback.setVisibility(View.GONE); advance.setVisibility(View.GONE); stop.setVisibility(View.GONE); shareCard.setVisibility(View.GONE); sendAppEntry.setVisibility(View.GONE); @@ -566,9 +568,10 @@ private void renderStartState(String ip, boolean twoCode) { // ------------------------------------------------------------ "Send the app" (ADFA-4785) private void renderSendApp() { + if (mode == Mode.HOTSPOT) ensureHotspot(); startApkServer(); NetworkInterfaces.LanIps net = NetworkInterfaces.discover(); - String ip = (net.hotspotIp != null) ? net.hotspotIp : net.wifiIp; + String ip = (mode == Mode.HOTSPOT) ? net.hotspotIp : net.wifiIp; if (ip == null || apkServer == null) { sendAppQr.setImageBitmap(null); return; } String url = "http://" + ip + ":" + shareConfig.apkPort + "/" + apkFileName; sendAppQr.setImageBitmap(SyncHandshakeHelper.generateQrCode(url, 500)); From 4417aaffab8f836a1085769f87cc1df49054f39c Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Tue, 21 Jul 2026 15:58:53 -0600 Subject: [PATCH 11/11] ADFA-4785: confirm before swapping network mid-share (step 3) Switching Hotspot<->Wi-Fi while sharing at step 3 drops the active connection and cuts any copy in progress. Route the tab taps through requestMode(): if a share is active at Copy, ask 'Switch to Hotspot/Wi-Fi?' ('This stops any copy in progress. The other phone can start again by rescanning the new code.') with Cancel / Switch; otherwise switch directly. Also reword the Stop dialog to 'This stops any copy in progress.' --- .../controller/redesign/CloneFragment.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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 55f19064..b750e59d 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 @@ -210,8 +210,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c tabSend.setOnClickListener(x -> setSide(Side.SEND)); tabReceive.setOnClickListener(x -> setSide(Side.RECEIVE)); - tabHotspot.setOnClickListener(x -> setMode(Mode.HOTSPOT)); - tabWifi.setOnClickListener(x -> setMode(Mode.WIFI)); + tabHotspot.setOnClickListener(x -> requestMode(Mode.HOTSPOT)); + tabWifi.setOnClickListener(x -> requestMode(Mode.WIFI)); advance.setOnClickListener(x -> { if (stage == Stage.JOIN) { sendApp = true; render(); } // step 1 -> step 2 (Get app) else { stage = Stage.JOIN; getAppDone = false; render(); } // step 3 -> back to step 1 @@ -274,6 +274,20 @@ private void setSide(Side sd) { render(); } + // ADFA-4785: switching the network at step 3 (Copy) drops the active connection and cuts any + // copy in progress. Warn first; elsewhere (or when the mode is unchanged) switch directly. + private void requestMode(Mode target) { + boolean sharing = (side == Side.SEND && !sendApp && stage == Stage.START && daemonStarted); + if (target == mode || !sharing) { setMode(target); return; } + String label = (target == Mode.HOTSPOT) ? "Hotspot" : "Wi-Fi"; + new AlertDialog.Builder(requireContext()) + .setTitle("Switch to " + label + "?") + .setMessage("This stops any copy in progress. The other phone can start again by rescanning the new code.") + .setNegativeButton("Cancel", null) + .setPositiveButton("Switch", (d, w) -> setMode(target)) + .show(); + } + private void setMode(Mode m) { mode = m; if (m == Mode.HOTSPOT) ensureHotspot(); @@ -887,7 +901,7 @@ public void onDestroyView() { private void confirmStop() { new AlertDialog.Builder(requireContext()) .setTitle("Stop sharing?") - .setMessage("This stops the copy. The other device can start again by re-scanning.") + .setMessage("This stops any copy in progress. The other device can start again by rescanning.") .setNegativeButton("Cancel", null) .setPositiveButton("Stop", (d, w) -> { if (transport != null) transport.stop();