From ae1377a04c2789c9329729443ca070c1a402b0db Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:14:28 -0600 Subject: [PATCH 01/55] =?UTF-8?q?ADFA-4725:=20new=20UI=20shell=20=E2=80=94?= =?UTF-8?q?=20scoped=20M3=20theme=20+=20LibraryActivity=20+=20bottom=20nav?= =?UTF-8?q?=20(Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/app/src/main/AndroidManifest.xml | 7 +++ .../org/iiab/controller/SplashActivity.java | 2 +- .../controller/redesign/LibraryActivity.java | 48 +++++++++++++++++++ .../redesign/PlaceholderFragment.java | 37 ++++++++++++++ .../app/src/main/res/color/k2go_nav_item.xml | 5 ++ .../src/main/res/drawable/ic_nav_clone.xml | 3 ++ .../src/main/res/drawable/ic_nav_connect.xml | 4 ++ .../src/main/res/drawable/ic_nav_library.xml | 3 ++ .../src/main/res/drawable/ic_nav_settings.xml | 4 ++ .../src/main/res/layout/activity_library.xml | 25 ++++++++++ .../res/layout/fragment_k2go_placeholder.xml | 29 +++++++++++ .../app/src/main/res/menu/bottom_nav_k2go.xml | 7 +++ .../src/main/res/values-night/colors_k2go.xml | 14 ++++++ .../app/src/main/res/values/colors_k2go.xml | 15 ++++++ .../app/src/main/res/values/strings_k2go.xml | 7 +++ .../app/src/main/res/values/themes_k2go.xml | 48 +++++++++++++++++++ 16 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java create mode 100644 controller/app/src/main/res/color/k2go_nav_item.xml create mode 100644 controller/app/src/main/res/drawable/ic_nav_clone.xml create mode 100644 controller/app/src/main/res/drawable/ic_nav_connect.xml create mode 100644 controller/app/src/main/res/drawable/ic_nav_library.xml create mode 100644 controller/app/src/main/res/drawable/ic_nav_settings.xml create mode 100644 controller/app/src/main/res/layout/activity_library.xml create mode 100644 controller/app/src/main/res/layout/fragment_k2go_placeholder.xml create mode 100644 controller/app/src/main/res/menu/bottom_nav_k2go.xml create mode 100644 controller/app/src/main/res/values-night/colors_k2go.xml create mode 100644 controller/app/src/main/res/values/colors_k2go.xml create mode 100644 controller/app/src/main/res/values/strings_k2go.xml create mode 100644 controller/app/src/main/res/values/themes_k2go.xml diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index 7917b9ae..22510055 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -80,6 +80,13 @@ tools:replace="screenOrientation" /> + + + diff --git a/controller/app/src/main/java/org/iiab/controller/SplashActivity.java b/controller/app/src/main/java/org/iiab/controller/SplashActivity.java index 86a7458f..f643d2eb 100644 --- a/controller/app/src/main/java/org/iiab/controller/SplashActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/SplashActivity.java @@ -64,7 +64,7 @@ protected void onCreate(Bundle savedInstanceState) { fade.setAlpha(0f); new Handler(Looper.getMainLooper()).postDelayed(() -> fade.animate().alpha(1f).setDuration(EXIT_FADE_MS).withEndAction(() -> { - startActivity(new Intent(SplashActivity.this, MainActivity.class)); + startActivity(new Intent(SplashActivity.this, org.iiab.controller.redesign.LibraryActivity.class)); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); finish(); }).start(), EXIT_AT_MS); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java new file mode 100644 index 00000000..95fa6f4f --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -0,0 +1,48 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import org.iiab.controller.R; + +/** + * New content-first UI shell (ADFA-4725): bottom nav Library / Connect / Clone / Settings. + * Phase 1 = shell only. Server wiring, content cards, wizard and Step-2 land in later phases. + */ +public class LibraryActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_library); + + BottomNavigationView nav = findViewById(R.id.k2go_bottom_nav); + nav.setOnItemSelectedListener(item -> { + showTab(item.getItemId()); + return true; + }); + + if (savedInstanceState == null) { + nav.setSelectedItemId(R.id.nav_library); + } + } + + private void showTab(int itemId) { + final String title; + if (itemId == R.id.nav_connect) { + title = "Connect"; + } else if (itemId == R.id.nav_clone) { + title = "Clone"; + } else if (itemId == R.id.nav_settings) { + title = "Settings"; + } else { + title = "Library"; + } + Fragment f = PlaceholderFragment.newInstance(title); + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_nav_host, f) + .commit(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java new file mode 100644 index 00000000..89ea2347 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java @@ -0,0 +1,37 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Temporary tab placeholder for the Phase-1 shell; replaced by real fragments in later phases. */ +public class PlaceholderFragment extends Fragment { + + private static final String ARG_TITLE = "title"; + + public static PlaceholderFragment newInstance(String title) { + PlaceholderFragment f = new PlaceholderFragment(); + Bundle b = new Bundle(); + b.putString(ARG_TITLE, title); + f.setArguments(b); + return f; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_k2go_placeholder, container, false); + TextView title = v.findViewById(R.id.k2go_placeholder_title); + if (getArguments() != null) { + title.setText(getArguments().getString(ARG_TITLE, "")); + } + return v; + } +} diff --git a/controller/app/src/main/res/color/k2go_nav_item.xml b/controller/app/src/main/res/color/k2go_nav_item.xml new file mode 100644 index 00000000..d686dcbe --- /dev/null +++ b/controller/app/src/main/res/color/k2go_nav_item.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_clone.xml b/controller/app/src/main/res/drawable/ic_nav_clone.xml new file mode 100644 index 00000000..6b2a1f38 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_clone.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_connect.xml b/controller/app/src/main/res/drawable/ic_nav_connect.xml new file mode 100644 index 00000000..9f02b657 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_connect.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_library.xml b/controller/app/src/main/res/drawable/ic_nav_library.xml new file mode 100644 index 00000000..870da117 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_library.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_settings.xml b/controller/app/src/main/res/drawable/ic_nav_settings.xml new file mode 100644 index 00000000..7c4ba1e0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/layout/activity_library.xml b/controller/app/src/main/res/layout/activity_library.xml new file mode 100644 index 00000000..52c6c292 --- /dev/null +++ b/controller/app/src/main/res/layout/activity_library.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml b/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml new file mode 100644 index 00000000..7dfcd7d3 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/controller/app/src/main/res/menu/bottom_nav_k2go.xml b/controller/app/src/main/res/menu/bottom_nav_k2go.xml new file mode 100644 index 00000000..f1699745 --- /dev/null +++ b/controller/app/src/main/res/menu/bottom_nav_k2go.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/controller/app/src/main/res/values-night/colors_k2go.xml b/controller/app/src/main/res/values-night/colors_k2go.xml new file mode 100644 index 00000000..d778cd06 --- /dev/null +++ b/controller/app/src/main/res/values-night/colors_k2go.xml @@ -0,0 +1,14 @@ + + + #FBF8F3 + #0F1A19 + #17302E + #4FB3BA + #2E9E5B + #F2A23C + #F2A23C + #C4462F + #9BA6A3 + #2A3D3B + #0F1A19 + diff --git a/controller/app/src/main/res/values/colors_k2go.xml b/controller/app/src/main/res/values/colors_k2go.xml new file mode 100644 index 00000000..8d03494d --- /dev/null +++ b/controller/app/src/main/res/values/colors_k2go.xml @@ -0,0 +1,15 @@ + + + + #14211F + #FBF8F3 + #EFEBE2 + #0E5C63 + #2E9E5B + #F2A23C + #B0740F + #C4462F + #5C6360 + #D9D4C8 + #FBF8F3 + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml new file mode 100644 index 00000000..9de46957 --- /dev/null +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -0,0 +1,7 @@ + + + Library + Connect + Clone + Settings + diff --git a/controller/app/src/main/res/values/themes_k2go.xml b/controller/app/src/main/res/values/themes_k2go.xml new file mode 100644 index 00000000..968b2864 --- /dev/null +++ b/controller/app/src/main/res/values/themes_k2go.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + From 7c2006ef14ba05a0eed9891abccb0cf3369935d7 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:25:37 -0600 Subject: [PATCH 02/55] =?UTF-8?q?ADFA-4725:=20runtime=20boot=20gate=20?= =?UTF-8?q?=E2=80=94=20own=20ServerController.Host,=20auto-start,=20Lottie?= =?UTF-8?q?=20entry=20gate=20(Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/app/build.gradle | 1 + .../controller/redesign/LibraryActivity.java | 128 ++++++++++++++++-- .../src/main/res/layout/activity_library.xml | 45 ++++-- .../src/main/res/raw/library_animation.json | 1 + 4 files changed, 152 insertions(+), 23 deletions(-) create mode 100755 controller/app/src/main/res/raw/library_animation.json diff --git a/controller/app/build.gradle b/controller/app/build.gradle index 84dd525f..277cefc2 100644 --- a/controller/app/build.gradle +++ b/controller/app/build.gradle @@ -167,6 +167,7 @@ dependencies { // 10. Core dependencies implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'com.google.android.material:material:1.13.0' + implementation 'com.airbnb.android:lottie:6.1.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.webkit:webkit:1.12.0' diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 95fa6f4f..4d8fc371 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -1,17 +1,40 @@ package org.iiab.controller.redesign; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Intent; import android.os.Bundle; -import androidx.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; import com.google.android.material.bottomnavigation.BottomNavigationView; import org.iiab.controller.R; +import org.iiab.controller.ServerController; +import org.iiab.controller.ServerStateRepository; +import org.iiab.controller.WatchdogService; /** - * New content-first UI shell (ADFA-4725): bottom nav Library / Connect / Clone / Settings. - * Phase 1 = shell only. Server wiring, content cards, wizard and Step-2 land in later phases. + * New content-first UI shell (ADFA-4725). Owns the server lifecycle for the new UI: + * starts the status poll, auto-starts the stack if it is down, and shows a boot gate + * (Lottie) that flips to OPEN once the server is reachable. + * Phase 2 = runtime gate. Content cards, wizard and Step-2 land in later phases. */ -public class LibraryActivity extends AppCompatActivity { +public class LibraryActivity extends AppCompatActivity implements ServerController.Host { + + private static final String TAG = "K2Go-Library"; + private static final long AUTOSTART_DELAY_MS = 3500L; + private static final long GATE_SAFETY_MS = 25000L; + + private ServerController serverController; + private boolean isNegotiating = false; + private Boolean targetServerState = null; + + private LottieAnimationView bootGate; + private boolean gateDismissed = false; @Override protected void onCreate(Bundle savedInstanceState) { @@ -23,10 +46,59 @@ protected void onCreate(Bundle savedInstanceState) { showTab(item.getItemId()); return true; }); - if (savedInstanceState == null) { nav.setSelectedItemId(R.id.nav_library); } + + bootGate = findViewById(R.id.k2go_boot_gate); + bootGate.setAnimation(R.raw.library_animation); + bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.playAnimation(); + + serverController = new ServerController(this, this); + serverController.start(); + + ServerStateRepository.get().state().observe(this, s -> { + if (s != null && s.alive) { + onServerReady(); + } + }); + + Handler main = new Handler(Looper.getMainLooper()); + // If the stack isn't up after one poll cycle, start it. + main.postDelayed(() -> { + if (!isFinishing() + && !ServerStateRepository.get().current().alive + && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } + }, AUTOSTART_DELAY_MS); + // Safety: never trap the user behind the gate. + main.postDelayed(() -> { + if (!gateDismissed) { + onServerReady(); + } + }, GATE_SAFETY_MS); + } + + private void onServerReady() { + if (gateDismissed || bootGate == null) { + return; + } + gateDismissed = true; + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(0); + bootGate.setMinAndMaxFrame("B_OPEN_FLIP"); + bootGate.addAnimatorListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (bootGate != null) { + bootGate.setVisibility(View.GONE); + } + } + }); + bootGate.playAnimation(); } private void showTab(int itemId) { @@ -40,9 +112,49 @@ private void showTab(int itemId) { } else { title = "Library"; } - Fragment f = PlaceholderFragment.newInstance(title); getSupportFragmentManager().beginTransaction() - .replace(R.id.k2go_nav_host, f) + .replace(R.id.k2go_nav_host, PlaceholderFragment.newInstance(title)) .commit(); } + + @Override + protected void onResume() { + super.onResume(); + if (serverController != null) serverController.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + if (serverController != null) serverController.onPause(); + } + + // --- ServerController.Host (shell: pulses / LEDs are no-ops for now) -------- + @Override public void addToLog(String message) { Log.d(TAG, message); } + @Override public void startFusionPulse() { } + @Override public void startExitPulse() { } + @Override public void stopBtnProgress() { } + @Override public void updateConnectivityLeds(boolean wifiOn, boolean hotspotOn) { } + @Override public void refreshServerUi() { } + @Override public Boolean getTargetServerState() { return targetServerState; } + @Override public void setTargetServerState(Boolean target) { targetServerState = target; } + @Override public boolean isNegotiating() { return isNegotiating; } + + @Override + public void enableSystemProtection() { + Intent i = new Intent(this, WatchdogService.class); + i.setAction(WatchdogService.ACTION_START); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + startForegroundService(i); + } else { + startService(i); + } + } + + @Override + public void disableSystemProtection() { + Intent i = new Intent(this, WatchdogService.class); + i.setAction(WatchdogService.ACTION_STOP); + startService(i); + } } diff --git a/controller/app/src/main/res/layout/activity_library.xml b/controller/app/src/main/res/layout/activity_library.xml index 52c6c292..5bc27aa7 100644 --- a/controller/app/src/main/res/layout/activity_library.xml +++ b/controller/app/src/main/res/layout/activity_library.xml @@ -1,25 +1,40 @@ - - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + + + - + android:clickable="true" + android:focusable="true" + android:scaleType="centerCrop" /> + diff --git a/controller/app/src/main/res/raw/library_animation.json b/controller/app/src/main/res/raw/library_animation.json new file mode 100755 index 00000000..015125c4 --- /dev/null +++ b/controller/app/src/main/res/raw/library_animation.json @@ -0,0 +1 @@ +{"v":"5.12.2","fr":30,"ip":0,"op":210,"w":1080,"h":1920,"nm":"Neighborhood Library","ddd":0,"assets":[],"fonts":{"list":[{"fName":"AtkinsonHyperlegible","fFamily":"Atkinson Hyperlegible","fStyle":"Bold","fWeight":"700","ascent":73,"origin":1,"fPath":"","fClass":""}]},"markers":[{"tm":0,"cm":"A_ENTRY_LOOP","dr":75},{"tm":75,"cm":"B_OPEN_FLIP","dr":30},{"tm":105,"cm":"C_EXIT_LOOP","dr":75},{"tm":180,"cm":"D_CLOSED_FLIP","dr":30}],"layers":[{"ddd":0,"ind":1,"ty":5,"nm":"SIGN_LABEL","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[540,742,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":90,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":195,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[100,100,100]}]}},"ao":0,"t":{"d":{"k":[{"t":0,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"CLOSED","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}},{"t":90,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"OPEN","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}},{"t":195,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"CLOSED","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0]}},"a":[]},"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"SIGN_BOARD","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[540,740,0]},"a":{"a":0,"k":[540,740,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":90,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":195,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[246,92]},"p":{"a":0,"k":[540,740]},"r":{"a":0,"k":14},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[216,64]},"p":{"a":0,"k":[540,740]},"r":{"a":0,"k":9},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":2},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"FLY_1","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":111,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":1,"k":[{"t":105,"s":[-20],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":148,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[-20]}]},"p":{"a":1,"k":[{"t":105,"s":[420,1450,0],"i":{"x":[0.3],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":130,"s":[460,1267.5,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[420,1225,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[420,1225,0],"h":1},{"t":180,"s":[420,1450,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"FLY_2","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":111,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":1,"k":[{"t":105,"s":[16],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":148,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[16]}]},"p":{"a":1,"k":[{"t":105,"s":[648,1452,0],"i":{"x":[0.3],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":130,"s":[664,1191,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[600,1070,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[600,1070,0],"h":1},{"t":180,"s":[648,1452,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"BOOK_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[-12,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[7,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[416,910]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"BOOK_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[10,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[-8,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[452,1065]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"BOOK_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[-9,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[10,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[720,1220]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"SHELF","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[560,620]},"p":{"a":0,"k":[540,1110]},"r":{"a":0,"k":18},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,965],[800,965]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,1120],[800,1120]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,1275],[800,1275]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[616,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[648,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[692,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[724,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[756,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[788,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[500,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[532,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[680,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[712,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[744,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[776,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[484,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[516,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[548,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[580,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[656,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[688,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"FLOOR_BOOKS","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":112,"s":[95],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[95],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":168,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[682,1452],[720,1434],[758,1452]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[720,1434],[720,1452]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[682,1452],[690,1446]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[758,1452],[750,1446]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[267.7,1456],[300,1440.7],[332.3,1456]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[300,1440.7],[300,1456]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[267.7,1456],[274.5,1450.9]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[332.3,1456],[325.5,1450.9]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"LAMP","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[500,450],[580,450]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[540,450],[540,540]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":true,"v":[[504,596],[576,596],[562,540],[518,540]],"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[540,646],[540,662]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[500,662],[580,662]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[512,662],[516,696]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[568,662],[564,696]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"FLOOR","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[190,1472],[890,1472]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"GLOW_CORE","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":22,"s":[6],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":50,"s":[58],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[80],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[85],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[66],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[85],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[360,300]},"p":{"a":0,"k":[540,660]},"nm":"el"},{"ty":"st","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"GLOW_ROOM","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":22,"s":[3],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":50,"s":[30],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[42],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[44],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[30],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[44],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[780,940]},"p":{"a":0,"k":[540,1000]},"nm":"el"},{"ty":"st","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0}]} \ No newline at end of file From e3cfcea69014516bc6c3822de67f0a58ccab0871 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:32:59 -0600 Subject: [PATCH 03/55] =?UTF-8?q?ADFA-4725:=20content-first=20Library=20ho?= =?UTF-8?q?me=20=E2=80=94=205=20cards,=203-state=20status=20dots,=20open?= =?UTF-8?q?=20Kiwix=20(Phase=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/redesign/LibraryActivity.java | 5 +- .../redesign/LibraryHomeFragment.java | 176 ++++++++++++++++++ .../src/main/res/drawable/ic_card_book.xml | 3 + .../src/main/res/drawable/ic_card_code.xml | 3 + .../src/main/res/drawable/ic_card_courses.xml | 4 + .../src/main/res/drawable/ic_card_maps.xml | 4 + .../main/res/drawable/ic_card_wikipedia.xml | 4 + .../src/main/res/drawable/k2go_card_bg.xml | 5 + .../app/src/main/res/drawable/k2go_dot.xml | 5 + .../src/main/res/drawable/k2go_getmore_bg.xml | 6 + .../main/res/layout/fragment_k2go_library.xml | 66 +++++++ .../src/main/res/layout/view_k2go_card.xml | 52 ++++++ 12 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java create mode 100644 controller/app/src/main/res/drawable/ic_card_book.xml create mode 100644 controller/app/src/main/res/drawable/ic_card_code.xml create mode 100644 controller/app/src/main/res/drawable/ic_card_courses.xml create mode 100644 controller/app/src/main/res/drawable/ic_card_maps.xml create mode 100644 controller/app/src/main/res/drawable/ic_card_wikipedia.xml create mode 100644 controller/app/src/main/res/drawable/k2go_card_bg.xml create mode 100644 controller/app/src/main/res/drawable/k2go_dot.xml create mode 100644 controller/app/src/main/res/drawable/k2go_getmore_bg.xml create mode 100644 controller/app/src/main/res/layout/fragment_k2go_library.xml create mode 100644 controller/app/src/main/res/layout/view_k2go_card.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 4d8fc371..d1cbe058 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -112,8 +112,11 @@ private void showTab(int itemId) { } else { title = "Library"; } + androidx.fragment.app.Fragment f = (itemId == R.id.nav_library) + ? new LibraryHomeFragment() + : PlaceholderFragment.newInstance(title); getSupportFragmentManager().beginTransaction() - .replace(R.id.k2go_nav_host, PlaceholderFragment.newInstance(title)) + .replace(R.id.k2go_nav_host, f) .commit(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java new file mode 100644 index 00000000..6e511670 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java @@ -0,0 +1,176 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import org.iiab.controller.PortalActivity; +import org.iiab.controller.R; +import org.iiab.controller.ServerStateRepository; +import org.iiab.controller.config.BoxEndpoints; +import org.iiab.controller.util.AppExecutors; + +/** + * Content-first Library home (ADFA-4725, Phase 3): action cards with live 3-state + * status dots (gray = not installed / amber = starting / green = ready). Tapping a + * ready card opens its content in the portal WebView (Explore Wikipedia -> Kiwix). + */ +public class LibraryHomeFragment extends Fragment { + + private static final long POLL_MS = 3000L; + private static final int GRAY = 0, AMBER = 1, GREEN = 2; + + private static final class Card { + final String endpoint; final String title; final boolean requires64; final int iconRes; + View dot; TextView status; int state = GRAY; + Card(String e, String t, boolean r, int i) { endpoint = e; title = t; requires64 = r; iconRes = i; } + } + + private final List cards = new ArrayList<>(); + private final Handler main = new Handler(Looper.getMainLooper()); + private TextView homeStatus; + private View homeStatusDot; + + private final Runnable poll = new Runnable() { + @Override public void run() { + refreshStatuses(); + main.postDelayed(this, POLL_MS); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_library, container, false); + homeStatus = root.findViewById(R.id.k2go_home_status); + homeStatusDot = root.findViewById(R.id.k2go_home_status_dot); + + cards.clear(); + cards.add(new Card("books", "Read a book", false, R.drawable.ic_card_book)); + cards.add(new Card("code", "Code on the Go", false, R.drawable.ic_card_code)); + cards.add(new Card("kiwix", "Explore Wikipedia", true, R.drawable.ic_card_wikipedia)); + cards.add(new Card("kolibri", "Take courses", false, R.drawable.ic_card_courses)); + cards.add(new Card("maps", "Navigate maps", false, R.drawable.ic_card_maps)); + + buildCards(inflater, root.findViewById(R.id.k2go_cards)); + + root.findViewById(R.id.k2go_get_more).setOnClickListener(v -> + Toast.makeText(requireContext(), "Get more — coming soon", Toast.LENGTH_SHORT).show()); + return root; + } + + private void buildCards(LayoutInflater inflater, LinearLayout host) { + LinearLayout row = null; + for (int i = 0; i < cards.size(); i++) { + if (i % 2 == 0) { + row = new LinearLayout(requireContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + host.addView(row, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + } + final Card c = cards.get(i); + View card = inflater.inflate(R.layout.view_k2go_card, row, false); + ((ImageView) card.findViewById(R.id.k2go_card_icon)).setImageResource(c.iconRes); + ((TextView) card.findViewById(R.id.k2go_card_title)).setText(c.title); + c.dot = card.findViewById(R.id.k2go_card_dot); + c.status = card.findViewById(R.id.k2go_card_status); + card.setOnClickListener(v -> onCardClick(c)); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) card.getLayoutParams(); + lp.width = 0; lp.weight = 1f; + row.addView(card, lp); + applyState(c, GRAY); + } + if (cards.size() % 2 == 1 && row != null) { + View spacer = new View(requireContext()); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, 1); + lp.weight = 1f; + row.addView(spacer, lp); + } + } + + private void onCardClick(Card c) { + if (c.state == GREEN) { + Intent i = new Intent(requireContext(), PortalActivity.class); + i.putExtra("TARGET_URL", BoxEndpoints.BASE + "/" + c.endpoint + "/"); + startActivity(i); + } else { + Toast.makeText(requireContext(), "Still starting — one moment", Toast.LENGTH_SHORT).show(); + } + } + + @Override public void onResume() { super.onResume(); main.post(poll); } + @Override public void onPause() { super.onPause(); main.removeCallbacks(poll); } + + private void refreshStatuses() { + boolean alive = ServerStateRepository.get().current().alive; + if (homeStatus != null) homeStatus.setText(alive ? "Ready to explore" : "Starting your library…"); + if (homeStatusDot != null) tint(homeStatusDot, alive ? R.color.k2go_leaf : R.color.k2go_amber); + for (final Card c : cards) { + if (c.requires64 && android.os.Build.SUPPORTED_64_BIT_ABIS.length == 0) { + applyState(c, GRAY); + if (c.status != null) c.status.setText("Not supported"); + continue; + } + if (!alive) { applyState(c, GRAY); continue; } + AppExecutors.get().io().execute(() -> { + final int st = probe(c.endpoint); + main.post(() -> { if (isAdded()) applyState(c, st); }); + }); + } + } + + private void applyState(Card c, int st) { + c.state = st; + if (c.dot == null || c.status == null) return; + int dotColor, textColor; + String label; + switch (st) { + case GREEN: dotColor = R.color.k2go_leaf; textColor = R.color.k2go_leaf; label = "Ready"; break; + case AMBER: dotColor = R.color.k2go_amber; textColor = R.color.k2go_amber_text; label = "Connecting"; break; + default: dotColor = R.color.k2go_muted; textColor = R.color.k2go_muted; label = "Not installed"; break; + } + tint(c.dot, dotColor); + c.status.setText(label); + c.status.setTextColor(ContextCompat.getColor(requireContext(), textColor)); + } + + private void tint(View v, int colorRes) { + v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + } + + private static int probe(String endpoint) { + HttpURLConnection c = null; + try { + URL u = new URL(BoxEndpoints.BASE + "/" + endpoint + "/"); + c = (HttpURLConnection) u.openConnection(); + c.setUseCaches(false); + c.setConnectTimeout(1500); + c.setReadTimeout(1500); + c.setRequestMethod("GET"); + int code = c.getResponseCode(); + if (code >= 200 && code < 400) return GREEN; + if (code == 404) return GRAY; + return AMBER; + } catch (Exception e) { + return AMBER; + } finally { + if (c != null) c.disconnect(); + } + } +} diff --git a/controller/app/src/main/res/drawable/ic_card_book.xml b/controller/app/src/main/res/drawable/ic_card_book.xml new file mode 100644 index 00000000..f4b7c1b2 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_book.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_card_code.xml b/controller/app/src/main/res/drawable/ic_card_code.xml new file mode 100644 index 00000000..11092566 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_code.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_card_courses.xml b/controller/app/src/main/res/drawable/ic_card_courses.xml new file mode 100644 index 00000000..a6301fc0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_courses.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_card_maps.xml b/controller/app/src/main/res/drawable/ic_card_maps.xml new file mode 100644 index 00000000..df3bbb09 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_maps.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_card_wikipedia.xml b/controller/app/src/main/res/drawable/ic_card_wikipedia.xml new file mode 100644 index 00000000..ea9306a0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_wikipedia.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/k2go_card_bg.xml b/controller/app/src/main/res/drawable/k2go_card_bg.xml new file mode 100644 index 00000000..2bf70a1b --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_card_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_dot.xml b/controller/app/src/main/res/drawable/k2go_dot.xml new file mode 100644 index 00000000..cea8990b --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_dot.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_getmore_bg.xml b/controller/app/src/main/res/drawable/k2go_getmore_bg.xml new file mode 100644 index 00000000..765156e0 --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_getmore_bg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_library.xml b/controller/app/src/main/res/layout/fragment_k2go_library.xml new file mode 100644 index 00000000..5d30cfb6 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_library.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/view_k2go_card.xml b/controller/app/src/main/res/layout/view_k2go_card.xml new file mode 100644 index 00000000..ec465321 --- /dev/null +++ b/controller/app/src/main/res/layout/view_k2go_card.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + From 945199a0479c40bf9a140b41a9821c36d772a95a Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:48:23 -0600 Subject: [PATCH 04/55] =?UTF-8?q?ADFA-4725:=20route=20first-run=20wizard?= =?UTF-8?q?=20to=20the=20new=20UI=20=E2=80=94=20LibraryActivity=20setup=20?= =?UTF-8?q?gate=20+=20SetupSectionFragment=20completion=20(Phase=204a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/iiab/controller/SetupSectionFragment.java | 2 +- .../org/iiab/controller/redesign/LibraryActivity.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java index b202ec54..4e473a10 100644 --- a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java @@ -243,7 +243,7 @@ private void completeSetup() { SharedPreferences prefs = requireContext().getSharedPreferences( getString(R.string.pref_file_internal), Context.MODE_PRIVATE); prefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); - startActivity(new Intent(requireContext(), MainActivity.class)); + startActivity(new Intent(requireContext(), org.iiab.controller.redesign.LibraryActivity.class)); requireActivity().finish(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index d1cbe058..e538c3be 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -3,6 +3,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -39,6 +40,16 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Not set up yet? Run the first-run wizard, then it routes back here. + SharedPreferences prefs0 = getSharedPreferences( + getString(R.string.pref_file_internal), MODE_PRIVATE); + if (!prefs0.getBoolean(getString(R.string.pref_key_setup_complete), false)) { + startActivity(new Intent(this, org.iiab.controller.SetupActivity.class)); + finish(); + return; + } + setContentView(R.layout.activity_library); BottomNavigationView nav = findViewById(R.id.k2go_bottom_nav); From 187040ee8d0cb6fe486ab3e4e5a12f484883379c Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:50:38 -0600 Subject: [PATCH 05/55] =?UTF-8?q?ADFA-4725:=20fix=20lint=20UseAppTint=20?= =?UTF-8?q?=E2=80=94=20app:tint=20on=20the=20Library=20card=20icon=20(Phas?= =?UTF-8?q?e=203=20follow-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/app/src/main/res/layout/view_k2go_card.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/res/layout/view_k2go_card.xml b/controller/app/src/main/res/layout/view_k2go_card.xml index ec465321..76d9f4c8 100644 --- a/controller/app/src/main/res/layout/view_k2go_card.xml +++ b/controller/app/src/main/res/layout/view_k2go_card.xml @@ -1,5 +1,6 @@ Date: Fri, 17 Jul 2026 06:54:13 -0600 Subject: [PATCH 06/55] ADFA-4725: content language follows the system + normalize legacy ISO codes (fix auto-detect fallback to en) (Phase 4b) --- .../iiab/controller/InstallationPlanner.java | 4 +- .../applang/data/ContentLanguage.java | 41 +++++++++++++++++++ .../install/presentation/InstallService.java | 2 +- .../presentation/PlannerController.java | 4 +- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java diff --git a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java index 786e5573..129d2283 100644 --- a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java +++ b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java @@ -223,10 +223,10 @@ private static void computeProjection(Context context, Tier tier, boolean pullCo @Override public void onReady(JSONObject catalog) { double kiwixSize = 0.0; - String resolvedLang = langCode; + String resolvedLang = org.iiab.controller.applang.data.ContentLanguage.normalize(langCode); String resolvedFile = null; - JSONObject langData = catalog.optJSONObject(langCode); + JSONObject langData = catalog.optJSONObject(resolvedLang); if (langData == null) { langData = catalog.optJSONObject("en"); diff --git a/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java b/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java new file mode 100644 index 00000000..1cd0a63b --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java @@ -0,0 +1,41 @@ +package org.iiab.controller.applang.data; + +import android.content.res.Resources; +import android.os.LocaleList; + +import java.util.Locale; + +/** + * Content-language helper (ADFA-4725). Two jobs: + * - normalize Java's legacy ISO-639 codes to the modern ones the Kiwix catalog uses + * (iw->he, in->id, ji->yi), and + * - derive the device/system UI language as the default content language. + * Fixes the "auto-detect isn't working" report: install-time consumers used to fall back + * to a hardcoded "en" and never re-derived from the system locale, and legacy codes never + * matched the catalog. + */ +public final class ContentLanguage { + + private ContentLanguage() { + } + + /** Normalize a language code to the modern ISO-639 form; null/empty passes through. */ + public static String normalize(String code) { + if (code == null || code.isEmpty()) return code; + String c = code.toLowerCase(Locale.ROOT); + switch (c) { + case "iw": return "he"; + case "in": return "id"; + case "ji": return "yi"; + default: return c; + } + } + + /** The system UI language as a normalized content-language code (e.g. "es"), or "en". */ + public static String systemDefault() { + LocaleList locales = Resources.getSystem().getConfiguration().getLocales(); + if (locales.isEmpty()) return "en"; + String lang = locales.get(0).getLanguage(); + return (lang == null || lang.isEmpty()) ? "en" : normalize(lang); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java index 0904f50b..c32b5707 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java @@ -307,7 +307,7 @@ public void onError(String error) { private void startCompanionData() { editLocalVarsForMaps(debianRootfs, tier); SharedPreferences prefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE); - String targetLang = (overrideKiwixLang != null) ? overrideKiwixLang : prefs.getString("selected_lang_minimal", "en"); + String targetLang = (overrideKiwixLang != null) ? overrideKiwixLang : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault()); InstallationPlanner.calculateProjectedSize(this, tier, true, targetLang, overrideKiwixVariant, new InstallationPlanner.PlanResultListener() { 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 index 972ff621..82b501d2 100644 --- 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 @@ -309,7 +309,7 @@ private void onRootfsSizeResolved(RootfsUiState rootfsState) { } 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", "en"); + 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() { @@ -474,7 +474,7 @@ public void onReady(JSONObject catalog) { 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", "en"); + 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); From b57e5809cdd95f7da695a5fdd2db31733c80df88 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:59:07 -0600 Subject: [PATCH 07/55] =?UTF-8?q?ADFA-4725:=20Set=20up=20your=20library=20?= =?UTF-8?q?=E2=80=94=20Step=201=20System=20(editions=20+=20live=20storage?= =?UTF-8?q?=20bar),=20Get=20more=20entry=20(Phase=205.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/app/src/main/AndroidManifest.xml | 6 + .../redesign/LibraryHomeFragment.java | 2 +- .../redesign/SetupLibraryActivity.java | 38 +++++ .../redesign/Step1SystemFragment.java | 130 ++++++++++++++++++ .../iiab/controller/redesign/StorageInfo.java | 13 ++ .../src/main/res/drawable/ic_radio_off.xml | 3 + .../app/src/main/res/drawable/ic_radio_on.xml | 4 + .../src/main/res/drawable/k2go_chip_bg.xml | 5 + .../src/main/res/drawable/k2go_primary_bg.xml | 5 + .../main/res/layout/activity_k2go_setup.xml | 6 + .../res/layout/fragment_k2go_setup_step1.xml | 92 +++++++++++++ .../src/main/res/layout/view_k2go_edition.xml | 71 ++++++++++ 12 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java create mode 100644 controller/app/src/main/res/drawable/ic_radio_off.xml create mode 100644 controller/app/src/main/res/drawable/ic_radio_on.xml create mode 100644 controller/app/src/main/res/drawable/k2go_chip_bg.xml create mode 100644 controller/app/src/main/res/drawable/k2go_primary_bg.xml create mode 100644 controller/app/src/main/res/layout/activity_k2go_setup.xml create mode 100644 controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml create mode 100644 controller/app/src/main/res/layout/view_k2go_edition.xml diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index 22510055..b3c8de85 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -87,6 +87,12 @@ android:exported="false" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:theme="@style/Theme.K2Go" /> + + diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java index 6e511670..50b16c04 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java @@ -71,7 +71,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c buildCards(inflater, root.findViewById(R.id.k2go_cards)); root.findViewById(R.id.k2go_get_more).setOnClickListener(v -> - Toast.makeText(requireContext(), "Get more — coming soon", Toast.LENGTH_SHORT).show()); + startActivity(new android.content.Intent(requireContext(), SetupLibraryActivity.class))); return root; } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java new file mode 100644 index 00000000..66a7ae57 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -0,0 +1,38 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; + +/** + * "Set up your library" host (ADFA-4725): Step 1 System -> Step 2 Content. Reached from the + * Library "Get more" action (and, later, the first-run wizard). Own Material 3 theme. + */ +public class SetupLibraryActivity extends AppCompatActivity { + + private InstallationPlanner.Tier selectedTier = InstallationPlanner.Tier.STANDARD; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_k2go_setup); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, new Step1SystemFragment()) + .commit(); + } + } + + public void setSelectedTier(InstallationPlanner.Tier tier) { this.selectedTier = tier; } + public InstallationPlanner.Tier getSelectedTier() { return selectedTier; } + + /** Called by Step 1 "Next". Step 2 (A/B) lands in the next increment. */ + public void goToStep2() { + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, + PlaceholderFragment.newInstance("Step 2 · Content — coming next")) + .addToBackStack("step2") + .commit(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java new file mode 100644 index 00000000..0b985df7 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -0,0 +1,130 @@ +package org.iiab.controller.redesign; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.ContentLanguage; + +/** Step 1 of "Set up your library": choose the system edition (apps) with a live storage bar. */ +public class Step1SystemFragment extends Fragment { + + private static final class Edition { + final InstallationPlanner.Tier tier; final String name; final double sizeGb; + final String desc; final boolean recommended; ImageView radio; + Edition(InstallationPlanner.Tier t, String n, double s, String d, boolean r) { + tier = t; name = n; sizeGb = s; desc = d; recommended = r; + } + } + + private final List editions = new ArrayList<>(); + private InstallationPlanner.Tier selected = InstallationPlanner.Tier.STANDARD; + private View barUsed, barSystem, barFree; + private LinearLayout bar; + private TextView legend; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_setup_step1, container, false); + bar = root.findViewById(R.id.k2go_storage_bar); + barUsed = root.findViewById(R.id.k2go_bar_used); + barSystem = root.findViewById(R.id.k2go_bar_system); + barFree = root.findViewById(R.id.k2go_bar_free); + legend = root.findViewById(R.id.k2go_storage_legend); + tint(barUsed, R.color.k2go_muted); + tint(barSystem, R.color.k2go_teal); + tint(barFree, R.color.k2go_hairline); + + editions.clear(); + editions.add(new Edition(InstallationPlanner.Tier.BASIC, "Basic", 1.2, + "Wikipedia reader app + Books app", false)); + editions.add(new Edition(InstallationPlanner.Tier.STANDARD, "Standard", 1.4, + "Basic plus Courses app", true)); + editions.add(new Edition(InstallationPlanner.Tier.FULL, "Full", 2.7, + "Standard plus Maps app (all apps)", false)); + + LinearLayout host = root.findViewById(R.id.k2go_editions); + for (Edition e : editions) { + View row = inflater.inflate(R.layout.view_k2go_edition, host, false); + ((TextView) row.findViewById(R.id.k2go_edition_name)).setText(e.name); + ((TextView) row.findViewById(R.id.k2go_edition_desc)).setText(e.desc); + ((TextView) row.findViewById(R.id.k2go_edition_size)) + .setText(String.format(Locale.US, "~ %.1f GB", e.sizeGb)); + row.findViewById(R.id.k2go_edition_reco) + .setVisibility(e.recommended ? View.VISIBLE : View.GONE); + e.radio = row.findViewById(R.id.k2go_edition_radio); + row.setOnClickListener(v -> select(e.tier)); + host.addView(row); + } + + root.findViewById(R.id.k2go_step1_next).setOnClickListener(v -> { + if (getActivity() instanceof SetupLibraryActivity) { + ((SetupLibraryActivity) getActivity()).setSelectedTier(selected); + ((SetupLibraryActivity) getActivity()).goToStep2(); + } + }); + + select(selected); + return root; + } + + private void select(InstallationPlanner.Tier tier) { + selected = tier; + if (getActivity() instanceof SetupLibraryActivity) { + ((SetupLibraryActivity) getActivity()).setSelectedTier(tier); + } + for (Edition e : editions) { + if (e.radio != null) { + e.radio.setImageResource(e.tier == tier ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + } + } + recomputeBar(tier); + } + + private void recomputeBar(InstallationPlanner.Tier tier) { + // System size (no content) resolves without a network read. + InstallationPlanner.calculateProjectedSize(requireContext(), tier, false, + ContentLanguage.systemDefault(), null, + projection -> { + if (!isAdded()) return; + applyBar(projection.osSize); + }); + } + + private void applyBar(double systemGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb); + if (total <= 0) total = used + systemGb + freeAfter + 0.01; + bar.setWeightSum((float) total); + setWeight(barUsed, (float) used); + setWeight(barSystem, (float) systemGb); + setWeight(barFree, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Free %.1f", + used, systemGb, freeAfter)); + } + + private void setWeight(View v, float w) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); + lp.weight = w; + v.setLayoutParams(lp); + } + + private void tint(View v, int colorRes) { + v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java b/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java new file mode 100644 index 00000000..59114130 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java @@ -0,0 +1,13 @@ +package org.iiab.controller.redesign; + +import android.os.Environment; + +/** Device storage in GB, from the app data partition (matches the old PlannerController source). */ +public final class StorageInfo { + private StorageInfo() { } + private static final double GB = 1024d * 1024d * 1024d; + + public static double totalGb() { return Environment.getDataDirectory().getTotalSpace() / GB; } + public static double freeGb() { return Environment.getDataDirectory().getFreeSpace() / GB; } + public static double usedGb() { return Math.max(0, totalGb() - freeGb()); } +} diff --git a/controller/app/src/main/res/drawable/ic_radio_off.xml b/controller/app/src/main/res/drawable/ic_radio_off.xml new file mode 100644 index 00000000..df1cc80f --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_radio_off.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_radio_on.xml b/controller/app/src/main/res/drawable/ic_radio_on.xml new file mode 100644 index 00000000..318dd7dd --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_radio_on.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/k2go_chip_bg.xml b/controller/app/src/main/res/drawable/k2go_chip_bg.xml new file mode 100644 index 00000000..71a3cf30 --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_chip_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_primary_bg.xml b/controller/app/src/main/res/drawable/k2go_primary_bg.xml new file mode 100644 index 00000000..3670bbca --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_primary_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/layout/activity_k2go_setup.xml b/controller/app/src/main/res/layout/activity_k2go_setup.xml new file mode 100644 index 00000000..00d8c69f --- /dev/null +++ b/controller/app/src/main/res/layout/activity_k2go_setup.xml @@ -0,0 +1,6 @@ + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml new file mode 100644 index 00000000..bd938a5b --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/view_k2go_edition.xml b/controller/app/src/main/res/layout/view_k2go_edition.xml new file mode 100644 index 00000000..2580d14c --- /dev/null +++ b/controller/app/src/main/res/layout/view_k2go_edition.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + From 1ea06929c2c7f2970b3a1e7b0a42f937bdee5a54 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 06:59:39 -0600 Subject: [PATCH 08/55] ADFA-4725: fix Step 1 projection listener (PlanResultListener is not a SAM) (Phase 5.1 follow-up) --- .../iiab/controller/redesign/Step1SystemFragment.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java index 0b985df7..7b5ec7e8 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -99,9 +99,13 @@ private void recomputeBar(InstallationPlanner.Tier tier) { // System size (no content) resolves without a network read. InstallationPlanner.calculateProjectedSize(requireContext(), tier, false, ContentLanguage.systemDefault(), null, - projection -> { - if (!isAdded()) return; - applyBar(projection.osSize); + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection projection) { + if (isAdded()) applyBar(projection.osSize); + } + @Override + public void onError(String error) { /* keep the last bar on error */ } }); } From ef9d165c7c18532cc4a692233a2dd1ef43f38956 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:03:31 -0600 Subject: [PATCH 09/55] ADFA-4725: Step 2 Content Option A (expandable + storage bar, real Kiwix sizes) + real Download via InstallService (Phase 5.2) --- .../redesign/SetupLibraryActivity.java | 3 +- .../redesign/Step2OptionAFragment.java | 165 ++++++++++++++++++ .../res/layout/fragment_k2go_setup_step2a.xml | 70 ++++++++ 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java create mode 100644 controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java index 66a7ae57..c6dbad1a 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -30,8 +30,7 @@ protected void onCreate(Bundle savedInstanceState) { /** Called by Step 1 "Next". Step 2 (A/B) lands in the next increment. */ public void goToStep2() { getSupportFragmentManager().beginTransaction() - .replace(R.id.k2go_setup_host, - PlaceholderFragment.newInstance("Step 2 · Content — coming next")) + .replace(R.id.k2go_setup_host, new Step2OptionAFragment()) .addToBackStack("step2") .commit(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java new file mode 100644 index 00000000..cff659c8 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -0,0 +1,165 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.Locale; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.iiab.controller.SystemStateEvaluator; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.install.presentation.InstallService; +import org.json.JSONObject; + +/** + * Step 2 Content — Option A (Expandable + storage Bar). Wikipedia coverage/detail compose a + * Kiwix variant with real catalog sizes; the bar + Download total come from InstallationPlanner; + * Download starts the real InstallService (tier + companion + lang + variant). + */ +public class Step2OptionAFragment extends Fragment { + + private boolean everything = false; // false = Popular + private boolean pictures = true; // true = With pictures + private String lang; + private JSONObject langData; // catalog entry for the language (nullable) + + private View barUsed, barSystem, barPicks, barFree; + private LinearLayout bar; + private TextView legend, wikiSize, download; + private TextView covPopular, covEverything, detPictures, detText; + + private InstallationPlanner.Tier tier() { + return (getActivity() instanceof SetupLibraryActivity) + ? ((SetupLibraryActivity) getActivity()).getSelectedTier() + : InstallationPlanner.Tier.STANDARD; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View v = inflater.inflate(R.layout.fragment_k2go_setup_step2a, c, false); + lang = ContentLanguage.systemDefault(); + bar = v.findViewById(R.id.k2go_bar); + barUsed = v.findViewById(R.id.k2go_bar_used); + barSystem = v.findViewById(R.id.k2go_bar_system); + barPicks = v.findViewById(R.id.k2go_bar_picks); + barFree = v.findViewById(R.id.k2go_bar_free); + legend = v.findViewById(R.id.k2go_legend); + wikiSize = v.findViewById(R.id.k2go_wiki_size); + download = v.findViewById(R.id.k2go_download); + covPopular = v.findViewById(R.id.k2go_cov_popular); + covEverything = v.findViewById(R.id.k2go_cov_everything); + detPictures = v.findViewById(R.id.k2go_det_pictures); + detText = v.findViewById(R.id.k2go_det_text); + tintBg(barUsed, R.color.k2go_muted); + tintBg(barSystem, R.color.k2go_teal); + tintBg(barPicks, R.color.k2go_leaf); + tintBg(barFree, R.color.k2go_hairline); + + covPopular.setOnClickListener(x -> { everything = false; refresh(); }); + covEverything.setOnClickListener(x -> { everything = true; refresh(); }); + detPictures.setOnClickListener(x -> { pictures = true; refresh(); }); + detText.setOnClickListener(x -> { pictures = false; refresh(); }); + download.setOnClickListener(x -> startDownload()); + + // Real Kiwix catalog for this language (cached; network only if stale). + InstallationPlanner.getOrFetchCatalog(requireContext(), catalog -> { + if (!isAdded()) return; + langData = catalog.optJSONObject(lang); + if (langData == null) langData = catalog.optJSONObject("en"); + refresh(); + }); + refresh(); + return v; + } + + private String popularBase() { + return (langData != null && langData.has("top1m_maxi")) ? "top1m" : "top"; + } + private String coverageBase() { return everything ? "all" : popularBase(); } + private String variantKey() { return coverageBase() + "_" + (pictures ? "maxi" : "nopic"); } + + private double sizeOf(String variant) { + if (langData == null) return -1; + JSONObject o = langData.optJSONObject(variant); + return o == null ? -1 : o.optDouble("size", -1); + } + private static String gb(double s) { return s >= 0 ? String.format(Locale.US, "%.1fG", s) : "—"; } + + private void refresh() { + // toggle labels with live sizes + label(covPopular, "Popular", sizeOf(popularBase() + "_" + (pictures ? "maxi" : "nopic")), !everything); + label(covEverything, "Everything", sizeOf("all_" + (pictures ? "maxi" : "nopic")), everything); + label(detPictures, "With pictures", sizeOf(coverageBase() + "_maxi"), pictures); + label(detText, "Text only", sizeOf(coverageBase() + "_nopic"), !pictures); + + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variantKey(), + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection p) { + if (!isAdded()) return; + wikiSize.setText(gb(p.kiwixSize)); + applyBar(p.osSize, p.mapsSize + p.kiwixSize); + download.setText(String.format(Locale.US, "Download library · %.1f GB", p.totalSize)); + } + @Override public void onError(String e) { } + }); + } + + private void label(TextView t, String name, double size, boolean on) { + t.setText(name + " " + gb(size)); + t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + t.setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_ink)); + } + + private void applyBar(double systemGb, double picksGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb - picksGb); + if (total <= 0) total = used + systemGb + picksGb + freeAfter + 0.01; + bar.setWeightSum((float) total); + setW(barUsed, (float) used); + setW(barSystem, (float) systemGb); + setW(barPicks, (float) picksGb); + setW(barFree, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Picks %.1f · Free %.1f", + used, systemGb, picksGb, freeAfter)); + } + + private void startDownload() { + Intent i = new Intent(requireContext(), InstallService.class); + i.setAction(InstallService.ACTION_START); + i.putExtra(InstallService.EXTRA_TIER, tier().name()); + i.putExtra(InstallService.EXTRA_COMPANION, true); + i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); + i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variantKey()); + i.putExtra(InstallService.EXTRA_REINSTALL, false); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + requireContext().startForegroundService(i); + } else { + requireContext().startService(i); + } + Toast.makeText(requireContext(), "Downloading your library…", Toast.LENGTH_LONG).show(); + startActivity(new Intent(requireContext(), LibraryActivity.class)); + requireActivity().finish(); + } + + private void setW(View v, float w) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); + lp.weight = w; v.setLayoutParams(lp); + } + private void tintBg(View v, int colorRes) { + v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + } +} diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml new file mode 100644 index 00000000..738e47be --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 853df568a4cb75ea576476ab41522fd34650aeac Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:06:24 -0600 Subject: [PATCH 10/55] ADFA-4725: fix Step 2 catalog listener (CacheListener is not a SAM) (Phase 5.2 follow-up) --- .../controller/redesign/Step2OptionAFragment.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index cff659c8..cb78e686 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -73,11 +73,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c download.setOnClickListener(x -> startDownload()); // Real Kiwix catalog for this language (cached; network only if stale). - InstallationPlanner.getOrFetchCatalog(requireContext(), catalog -> { - if (!isAdded()) return; - langData = catalog.optJSONObject(lang); - if (langData == null) langData = catalog.optJSONObject("en"); - refresh(); + InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { + @Override + public void onReady(JSONObject catalog) { + if (!isAdded()) return; + langData = catalog.optJSONObject(lang); + if (langData == null) langData = catalog.optJSONObject("en"); + refresh(); + } + @Override + public void onError(String error) { /* keep "—"; Download still resolves the variant */ } }); refresh(); return v; From ea05f4742e520ee4de7cd79fbdc92694e06fd264 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:10:54 -0600 Subject: [PATCH 11/55] ADFA-4725: Step 2 Option B (5-step + gauge) + hidden tap-5x A/B switch with shared picks (Phase 5.3) --- .../org/iiab/controller/redesign/AbFlip.java | 19 ++ .../redesign/SetupLibraryActivity.java | 32 +++- .../redesign/Step2OptionAFragment.java | 11 ++ .../redesign/Step2OptionBFragment.java | 180 ++++++++++++++++++ .../res/layout/fragment_k2go_setup_step2a.xml | 2 +- .../res/layout/fragment_k2go_setup_step2b.xml | 70 +++++++ 6 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java create mode 100644 controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java b/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java new file mode 100644 index 00000000..5ea4fdfd --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.SystemClock; +import android.view.View; + +/** Hidden A/B-test gesture: 5 taps on the title within 1.5s flips the Step-2 layout. */ +final class AbFlip { + private AbFlip() { } + static void attach(View title, Runnable onFlip) { + final int[] count = {0}; + final long[] last = {0}; + title.setOnClickListener(v -> { + long now = SystemClock.uptimeMillis(); + if (now - last[0] > 1500) count[0] = 0; + last[0] = now; + if (++count[0] >= 5) { count[0] = 0; onFlip.run(); } + }); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java index c6dbad1a..6c3f872e 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -1,17 +1,23 @@ package org.iiab.controller.redesign; import android.os.Bundle; +import android.util.Log; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; import org.iiab.controller.InstallationPlanner; import org.iiab.controller.R; /** - * "Set up your library" host (ADFA-4725): Step 1 System -> Step 2 Content. Reached from the - * Library "Get more" action (and, later, the first-run wizard). Own Material 3 theme. + * "Set up your library" host (ADFA-4725): Step 1 System -> Step 2 Content (A/B). Holds the + * shared tier + content picks so the two Step-2 layouts (A expandable+bar, B 5-step+gauge) + * carry selections across the hidden tap-5x flip. */ public class SetupLibraryActivity extends AppCompatActivity { private InstallationPlanner.Tier selectedTier = InstallationPlanner.Tier.STANDARD; + private boolean contentEverything = false; // false = Popular + private boolean contentPictures = true; // true = With pictures + private boolean optionB = false; // A is the default @Override protected void onCreate(Bundle savedInstanceState) { @@ -27,11 +33,29 @@ protected void onCreate(Bundle savedInstanceState) { public void setSelectedTier(InstallationPlanner.Tier tier) { this.selectedTier = tier; } public InstallationPlanner.Tier getSelectedTier() { return selectedTier; } - /** Called by Step 1 "Next". Step 2 (A/B) lands in the next increment. */ + public boolean isEverything() { return contentEverything; } + public void setEverything(boolean b) { contentEverything = b; } + public boolean isPictures() { return contentPictures; } + public void setPictures(boolean b) { contentPictures = b; } + + private Fragment step2Fragment() { + return optionB ? new Step2OptionBFragment() : new Step2OptionAFragment(); + } + + /** Called by Step 1 "Next". */ public void goToStep2() { getSupportFragmentManager().beginTransaction() - .replace(R.id.k2go_setup_host, new Step2OptionAFragment()) + .replace(R.id.k2go_setup_host, step2Fragment()) .addToBackStack("step2") .commit(); } + + /** Hidden A/B-test switch: flip the Step-2 layout in place; picks carry over. */ + public void flipAbTest() { + optionB = !optionB; + Log.d("K2Go-ABtest", "Set-up-content layout -> " + (optionB ? "B (5-step + gauge)" : "A (expandable + bar)")); + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, step2Fragment()) + .commit(); + } } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index cb78e686..b089408b 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -49,6 +49,13 @@ private InstallationPlanner.Tier tier() { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { View v = inflater.inflate(R.layout.fragment_k2go_setup_step2a, c, false); lang = ContentLanguage.systemDefault(); + if (getActivity() instanceof SetupLibraryActivity) { + everything = ((SetupLibraryActivity) getActivity()).isEverything(); + pictures = ((SetupLibraryActivity) getActivity()).isPictures(); + } + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { + if (getActivity() instanceof SetupLibraryActivity) ((SetupLibraryActivity) getActivity()).flipAbTest(); + }); bar = v.findViewById(R.id.k2go_bar); barUsed = v.findViewById(R.id.k2go_bar_used); barSystem = v.findViewById(R.id.k2go_bar_system); @@ -102,6 +109,10 @@ private double sizeOf(String variant) { private static String gb(double s) { return s >= 0 ? String.format(Locale.US, "%.1fG", s) : "—"; } private void refresh() { + if (getActivity() instanceof SetupLibraryActivity) { + ((SetupLibraryActivity) getActivity()).setEverything(everything); + ((SetupLibraryActivity) getActivity()).setPictures(pictures); + } // toggle labels with live sizes label(covPopular, "Popular", sizeOf(popularBase() + "_" + (pictures ? "maxi" : "nopic")), !everything); label(covEverything, "Everything", sizeOf("all_" + (pictures ? "maxi" : "nopic")), everything); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java new file mode 100644 index 00000000..9cab6a4b --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -0,0 +1,180 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.Locale; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.iiab.controller.SystemStateEvaluator; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.install.presentation.InstallService; + +/** + * Step 2 Content — Option B (5-step map + Gauge). Guided: one source per step, a projected-use + * gauge (real total from InstallationPlanner), Wikipedia coverage/detail on step 1, Download on + * Review. Same install payload as Option A; picks are shared via SetupLibraryActivity. + */ +public class Step2OptionBFragment extends Fragment { + + private static final String[] NAMES = {"Wikipedia", "Books", "Maps", "Courses", "Review"}; + private static final String[] INFO = { + "", "Books — included with your edition.", "Maps — included with your edition.", + "Courses — set up on the device after install.", "Review your library, then download."}; + + private int step = 0; + private String lang; + private boolean everything = false, pictures = true; + private double lastTotal = 0; + + private final TextView[] dots = new TextView[5]; + private TextView caption, gaugeTotal, legend, btnBack, btnNext, info; + private TextView covPop, covEvery, detPic, detTxt; + private View wikiBlock; + private LinearLayout bar; + private View bU, bS, bP, bF; + + private SetupLibraryActivity act() { + return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; + } + private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } + private String variantKey() { return (everything ? "all" : "top1m") + "_" + (pictures ? "maxi" : "nopic"); } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View v = inflater.inflate(R.layout.fragment_k2go_setup_step2b, c, false); + lang = ContentLanguage.systemDefault(); + if (act() != null) { everything = act().isEverything(); pictures = act().isPictures(); } + + int[] ids = {R.id.k2go_sd0, R.id.k2go_sd1, R.id.k2go_sd2, R.id.k2go_sd3, R.id.k2go_sd4}; + for (int i = 0; i < 5; i++) dots[i] = v.findViewById(ids[i]); + caption = v.findViewById(R.id.k2go_step_caption); + gaugeTotal = v.findViewById(R.id.k2go_gauge_total); + legend = v.findViewById(R.id.k2go_legend); + info = v.findViewById(R.id.k2go_info_text); + btnBack = v.findViewById(R.id.k2go_btn_back); + btnNext = v.findViewById(R.id.k2go_btn_next); + wikiBlock = v.findViewById(R.id.k2go_wiki_block); + bar = v.findViewById(R.id.k2go_bar); + bU = v.findViewById(R.id.k2go_bar_used); + bS = v.findViewById(R.id.k2go_bar_system); + bP = v.findViewById(R.id.k2go_bar_picks); + bF = v.findViewById(R.id.k2go_bar_free); + covPop = v.findViewById(R.id.k2go_cov_popular); + covEvery = v.findViewById(R.id.k2go_cov_everything); + detPic = v.findViewById(R.id.k2go_det_pictures); + detTxt = v.findViewById(R.id.k2go_det_text); + tintBg(bU, R.color.k2go_muted); + tintBg(bS, R.color.k2go_teal); + tintBg(bP, R.color.k2go_leaf); + tintBg(bF, R.color.k2go_hairline); + + covPop.setOnClickListener(x -> { everything = false; persist(); refreshProjection(); }); + covEvery.setOnClickListener(x -> { everything = true; persist(); refreshProjection(); }); + detPic.setOnClickListener(x -> { pictures = true; persist(); refreshProjection(); }); + detTxt.setOnClickListener(x -> { pictures = false; persist(); refreshProjection(); }); + btnBack.setOnClickListener(x -> { if (step > 0) { step--; render(); } }); + btnNext.setOnClickListener(x -> { if (step < 4) { step++; render(); } else startDownload(); }); + AbFlip.attach(caption != null ? v.findViewById(R.id.k2go_step2_title) : v, () -> { if (act() != null) act().flipAbTest(); }); + + render(); + refreshProjection(); + return v; + } + + private void persist() { + if (act() != null) { act().setEverything(everything); act().setPictures(pictures); } + toggleLabels(); + } + + private void render() { + for (int i = 0; i < 5; i++) { + boolean on = i == step; + dots[i].setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + dots[i].setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_muted)); + } + caption.setText(NAMES[step]); + wikiBlock.setVisibility(step == 0 ? View.VISIBLE : View.GONE); + info.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + info.setText(INFO[step]); + btnBack.setVisibility(step == 0 ? View.INVISIBLE : View.VISIBLE); + toggleLabels(); + updateNextLabel(); + } + + private void toggleLabels() { + hi(covPop, !everything); hi(covEvery, everything); + hi(detPic, pictures); hi(detTxt, !pictures); + } + private void hi(TextView t, boolean on) { + t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + t.setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_ink)); + } + + private void updateNextLabel() { + btnNext.setText(step == 4 + ? String.format(Locale.US, "Download library · %.1f GB", lastTotal) + : "Next"); + } + + private void refreshProjection() { + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variantKey(), + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection p) { + if (!isAdded()) return; + lastTotal = p.totalSize; + gaugeTotal.setText(String.format(Locale.US, "%.1f GB", p.totalSize)); + applyBar(p.osSize, p.mapsSize + p.kiwixSize); + updateNextLabel(); + } + @Override public void onError(String e) { } + }); + } + + private void applyBar(double systemGb, double picksGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb - picksGb); + if (total <= 0) total = used + systemGb + picksGb + freeAfter + 0.01; + bar.setWeightSum((float) total); + setW(bU, (float) used); setW(bS, (float) systemGb); setW(bP, (float) picksGb); setW(bF, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Picks %.1f · Free %.1f", + used, systemGb, picksGb, freeAfter)); + } + + private void startDownload() { + Intent i = new Intent(requireContext(), InstallService.class); + i.setAction(InstallService.ACTION_START); + i.putExtra(InstallService.EXTRA_TIER, tier().name()); + i.putExtra(InstallService.EXTRA_COMPANION, true); + i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); + i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variantKey()); + i.putExtra(InstallService.EXTRA_REINSTALL, false); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); + else requireContext().startService(i); + Toast.makeText(requireContext(), "Downloading your library…", Toast.LENGTH_LONG).show(); + startActivity(new Intent(requireContext(), LibraryActivity.class)); + requireActivity().finish(); + } + + private void setW(View v, float w) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); + lp.weight = w; v.setLayoutParams(lp); + } + private void tintBg(View v, int colorRes) { + v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + } +} diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 738e47be..74169478 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -6,7 +6,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From afcc95ff273d800d5bb7d7587b770ac331dc5a6f Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:19:15 -0600 Subject: [PATCH 12/55] ADFA-4725: Settings tab (About live + Advanced rows) + Turn off K2Go closing scene & graceful teardown (Phase 6.1) --- .../controller/redesign/LibraryActivity.java | 51 ++++++- .../controller/redesign/SettingsFragment.java | 141 ++++++++++++++++++ .../res/layout/fragment_k2go_settings.xml | 7 + 3 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java create mode 100644 controller/app/src/main/res/layout/fragment_k2go_settings.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index e538c3be..77052ac5 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -36,6 +36,8 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll private LottieAnimationView bootGate; private boolean gateDismissed = false; + private boolean closing = false; + private boolean closedDone = false; @Override protected void onCreate(Bundle savedInstanceState) { @@ -71,7 +73,10 @@ protected void onCreate(Bundle savedInstanceState) { serverController.start(); ServerStateRepository.get().state().observe(this, s -> { - if (s != null && s.alive) { + if (s == null) return; + if (closing) { + if (!s.alive) onClosedReady(); + } else if (s.alive) { onServerReady(); } }); @@ -123,9 +128,14 @@ private void showTab(int itemId) { } else { title = "Library"; } - androidx.fragment.app.Fragment f = (itemId == R.id.nav_library) - ? new LibraryHomeFragment() - : PlaceholderFragment.newInstance(title); + androidx.fragment.app.Fragment f; + if (itemId == R.id.nav_library) { + f = new LibraryHomeFragment(); + } else if (itemId == R.id.nav_settings) { + f = new SettingsFragment(); + } else { + f = PlaceholderFragment.newInstance(title); + } getSupportFragmentManager().beginTransaction() .replace(R.id.k2go_nav_host, f) .commit(); @@ -143,6 +153,39 @@ protected void onPause() { if (serverController != null) serverController.onPause(); } + /** Settings "Turn off K2Go": full-screen closing scene + graceful teardown, then leave. */ + public void turnOffK2Go() { + if (closing) return; + closing = true; + if (bootGate != null) { + bootGate.setVisibility(View.VISIBLE); + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.setMinAndMaxFrame("C_EXIT_LOOP"); + bootGate.playAnimation(); + } + if (ServerStateRepository.get().current().alive && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } else if (!ServerStateRepository.get().current().alive) { + onClosedReady(); + } + new Handler(Looper.getMainLooper()).postDelayed(this::onClosedReady, 15000L); + } + + private void onClosedReady() { + if (closedDone) return; + closedDone = true; + if (bootGate == null) { finishAndRemoveTask(); return; } + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(0); + bootGate.setMinAndMaxFrame("D_CLOSED_FLIP"); + bootGate.addAnimatorListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { finishAndRemoveTask(); } + }); + bootGate.playAnimation(); + } + // --- ServerController.Host (shell: pulses / LEDs are no-ops for now) -------- @Override public void addToLog(String message) { Log.d(TAG, message); } @Override public void startFusionPulse() { } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java new file mode 100644 index 00000000..d1f6ccb3 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -0,0 +1,141 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Settings tab (ADFA-4725): Basics (About live) + Advanced (visual) + Turn off K2Go. */ +public class SettingsFragment extends Fragment { + + private LinearLayout list; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_settings, c, false); + list = root.findViewById(R.id.k2go_settings_list); + + View.OnClickListener soon = v -> + Toast.makeText(requireContext(), "Coming soon", Toast.LENGTH_SHORT).show(); + + header("BASICS"); + row("Language", "", soon); + row("Permissions", "", soon); + row("Theme", "Light / Follow system / Dark", soon); + row("Share usage statistics", "On", soon); + row("About", versionName(), soon); + row("Send feedback", "", soon); + + header("ADVANCED · for power users"); + row("System & modules", "", soon); + row("Backups & recovery", "", soon); + row("Developer tools (ADB)", "", soon); + row("Network & DNS", "", soon); + row("Terminal (Debian)", "", soon); + + turnOffRow(); + return root; + } + + private String versionName() { + try { + return "v" + requireContext().getPackageManager() + .getPackageInfo(requireContext().getPackageName(), 0).versionName; + } catch (Exception e) { + return ""; + } + } + + private void header(String text) { + TextView t = new TextView(requireContext()); + t.setText(text); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.topMargin = dp(18); + lp.bottomMargin = dp(4); + list.addView(t, lp); + } + + private void row(String title, String value, View.OnClickListener onClick) { + LinearLayout row = new LinearLayout(requireContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setBackgroundResource(R.drawable.k2go_card_bg); + row.setPadding(dp(14), dp(14), dp(14), dp(14)); + row.setClickable(true); + row.setOnClickListener(onClick); + + TextView t = new TextView(requireContext()); + t.setText(title); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); + t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_ink)); + row.addView(t, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + + TextView val = new TextView(requireContext()); + val.setText(value == null ? "" : value); + val.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + val.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted)); + row.addView(val, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.topMargin = dp(8); + list.addView(row, lp); + } + + private void turnOffRow() { + TextView note = new TextView(requireContext()); + note.setText("Home/back only minimize — the library keeps running. Turn off to fully stop it."); + note.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + note.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted)); + LinearLayout.LayoutParams np = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + np.topMargin = dp(24); + list.addView(note, np); + + TextView off = new TextView(requireContext()); + off.setText("Turn off K2Go"); + off.setGravity(Gravity.CENTER); + off.setPadding(dp(16), dp(16), dp(16), dp(16)); + off.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); + off.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_clay)); + off.setBackgroundResource(R.drawable.k2go_getmore_bg); + off.setClickable(true); + off.setOnClickListener(v -> confirmTurnOff()); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.topMargin = dp(8); + list.addView(off, lp); + } + + private void confirmTurnOff() { + new AlertDialog.Builder(requireContext()) + .setTitle("Turn off K2Go?") + .setMessage("This stops the library and closes the app.") + .setNegativeButton("Cancel", null) + .setPositiveButton("Turn off", (d, w) -> { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).turnOffK2Go(); + } + }) + .show(); + } + + private int dp(int v) { + return Math.round(v * getResources().getDisplayMetrics().density); + } +} diff --git a/controller/app/src/main/res/layout/fragment_k2go_settings.xml b/controller/app/src/main/res/layout/fragment_k2go_settings.xml new file mode 100644 index 00000000..03e4d45b --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_settings.xml @@ -0,0 +1,7 @@ + + + + From 7902eaa87f221092af48bc368fb780fbb596de92 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:22:01 -0600 Subject: [PATCH 13/55] ADFA-4725: Connect + Clone designed shells (Phase 6.2) --- .../controller/redesign/CloneFragment.java | 19 ++++++ .../controller/redesign/ConnectFragment.java | 19 ++++++ .../controller/redesign/LibraryActivity.java | 4 ++ .../main/res/layout/fragment_k2go_clone.xml | 60 +++++++++++++++++++ .../main/res/layout/fragment_k2go_connect.xml | 52 ++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java create mode 100644 controller/app/src/main/res/layout/fragment_k2go_clone.xml create mode 100644 controller/app/src/main/res/layout/fragment_k2go_connect.xml 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 new file mode 100644 index 00000000..9bd3d7be --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Clone tab — designed shell (ADFA-4725, Phase 6). Live transfer lands later. */ +public class CloneFragment extends Fragment { + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + return inflater.inflate(R.layout.fragment_k2go_clone, c, false); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java new file mode 100644 index 00000000..c96d6b5c --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Connect tab — designed shell (ADFA-4725, Phase 6). Live hotspot/QR lands later. */ +public class ConnectFragment extends Fragment { + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + return inflater.inflate(R.layout.fragment_k2go_connect, c, false); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 77052ac5..2ce7e4fb 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -131,6 +131,10 @@ private void showTab(int itemId) { androidx.fragment.app.Fragment f; if (itemId == R.id.nav_library) { f = new LibraryHomeFragment(); + } else if (itemId == R.id.nav_connect) { + f = new ConnectFragment(); + } else if (itemId == R.id.nav_clone) { + f = new CloneFragment(); } else if (itemId == R.id.nav_settings) { f = new SettingsFragment(); } 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 new file mode 100644 index 00000000..07ed30e8 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_connect.xml b/controller/app/src/main/res/layout/fragment_k2go_connect.xml new file mode 100644 index 00000000..79354dc3 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_connect.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3f1d83a0dc8e5c33b04896be8394d4b1eb3f4d4d Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:25:26 -0600 Subject: [PATCH 14/55] =?UTF-8?q?ADFA-4725:=20polish=20=E2=80=94=20card=20?= =?UTF-8?q?'Unavailable=20=C2=B7=20tap=20to=20retry'=20+=20reduce-motion?= =?UTF-8?q?=20fallbacks=20for=20the=20boot/exit=20scenes=20(Phase=208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/redesign/LibraryActivity.java | 23 +++++++++++++++---- .../redesign/LibraryHomeFragment.java | 19 +++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 2ce7e4fb..94980b6b 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -64,10 +64,12 @@ protected void onCreate(Bundle savedInstanceState) { } bootGate = findViewById(R.id.k2go_boot_gate); - bootGate.setAnimation(R.raw.library_animation); - bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); - bootGate.setRepeatCount(LottieDrawable.INFINITE); - bootGate.playAnimation(); + if (!reduceMotion()) { + bootGate.setAnimation(R.raw.library_animation); + bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.playAnimation(); + } serverController = new ServerController(this, this); serverController.start(); @@ -103,6 +105,7 @@ private void onServerReady() { return; } gateDismissed = true; + if (reduceMotion()) { bootGate.setVisibility(View.GONE); return; } bootGate.removeAllAnimatorListeners(); bootGate.setRepeatCount(0); bootGate.setMinAndMaxFrame("B_OPEN_FLIP"); @@ -157,11 +160,20 @@ protected void onPause() { if (serverController != null) serverController.onPause(); } + private boolean reduceMotion() { + try { + return android.provider.Settings.Global.getFloat(getContentResolver(), + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f; + } catch (Exception e) { + return false; + } + } + /** Settings "Turn off K2Go": full-screen closing scene + graceful teardown, then leave. */ public void turnOffK2Go() { if (closing) return; closing = true; - if (bootGate != null) { + if (bootGate != null && !reduceMotion()) { bootGate.setVisibility(View.VISIBLE); bootGate.removeAllAnimatorListeners(); bootGate.setRepeatCount(LottieDrawable.INFINITE); @@ -180,6 +192,7 @@ private void onClosedReady() { if (closedDone) return; closedDone = true; if (bootGate == null) { finishAndRemoveTask(); return; } + if (reduceMotion()) { finishAndRemoveTask(); return; } bootGate.removeAllAnimatorListeners(); bootGate.setRepeatCount(0); bootGate.setMinAndMaxFrame("D_CLOSED_FLIP"); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java index 50b16c04..dcf55f0b 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java @@ -34,11 +34,11 @@ public class LibraryHomeFragment extends Fragment { private static final long POLL_MS = 3000L; - private static final int GRAY = 0, AMBER = 1, GREEN = 2; + private static final int GRAY = 0, AMBER = 1, GREEN = 2, RED = 3; private static final class Card { final String endpoint; final String title; final boolean requires64; final int iconRes; - View dot; TextView status; int state = GRAY; + View dot; TextView status; int state = GRAY; int amberStreak = 0; Card(String e, String t, boolean r, int i) { endpoint = e; title = t; requires64 = r; iconRes = i; } } @@ -110,7 +110,13 @@ private void onCardClick(Card c) { i.putExtra("TARGET_URL", BoxEndpoints.BASE + "/" + c.endpoint + "/"); startActivity(i); } else { - Toast.makeText(requireContext(), "Still starting — one moment", Toast.LENGTH_SHORT).show(); + c.amberStreak = 0; + applyState(c, AMBER); + AppExecutors.get().io().execute(() -> { + final int st = probe(c.endpoint); + main.post(() -> { if (isAdded()) applyState(c, (st == GREEN || st == GRAY) ? st : AMBER); }); + }); + Toast.makeText(requireContext(), "Retrying…", Toast.LENGTH_SHORT).show(); } } @@ -130,7 +136,11 @@ private void refreshStatuses() { if (!alive) { applyState(c, GRAY); continue; } AppExecutors.get().io().execute(() -> { final int st = probe(c.endpoint); - main.post(() -> { if (isAdded()) applyState(c, st); }); + main.post(() -> { + if (!isAdded()) return; + if (st == GREEN || st == GRAY) { c.amberStreak = 0; applyState(c, st); } + else { c.amberStreak++; applyState(c, c.amberStreak >= 4 ? RED : AMBER); } + }); }); } } @@ -143,6 +153,7 @@ private void applyState(Card c, int st) { switch (st) { case GREEN: dotColor = R.color.k2go_leaf; textColor = R.color.k2go_leaf; label = "Ready"; break; case AMBER: dotColor = R.color.k2go_amber; textColor = R.color.k2go_amber_text; label = "Connecting"; break; + case RED: dotColor = R.color.k2go_clay; textColor = R.color.k2go_clay; label = "Unavailable · tap to retry"; break; default: dotColor = R.color.k2go_muted; textColor = R.color.k2go_muted; label = "Not installed"; break; } tint(c.dot, dotColor); From 7d4ad7f5f4f30dc79e386705517b1ebfa60aa686 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:34:47 -0600 Subject: [PATCH 15/55] ADFA-4725: new first-run onboarding (Welcome/Language/Permissions/Set up library) in M3; startup no longer shows the legacy wizard (Phase 4 rework) --- controller/app/src/main/AndroidManifest.xml | 6 + .../controller/redesign/LibraryActivity.java | 2 +- .../controller/redesign/WizardActivity.java | 217 ++++++++++++++++++ .../main/res/layout/activity_k2go_wizard.xml | 85 +++++++ 4 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java create mode 100644 controller/app/src/main/res/layout/activity_k2go_wizard.xml diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index b3c8de85..0944d9fb 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -93,6 +93,12 @@ android:exported="false" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:theme="@style/Theme.K2Go" /> + + diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 94980b6b..9c9f4d51 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -47,7 +47,7 @@ protected void onCreate(Bundle savedInstanceState) { SharedPreferences prefs0 = getSharedPreferences( getString(R.string.pref_file_internal), MODE_PRIVATE); if (!prefs0.getBoolean(getString(R.string.pref_key_setup_complete), false)) { - startActivity(new Intent(this, org.iiab.controller.SetupActivity.class)); + startActivity(new Intent(this, WizardActivity.class)); finish(); return; } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java new file mode 100644 index 00000000..bda20571 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java @@ -0,0 +1,217 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.AppLocaleController; +import org.iiab.controller.applang.data.ContentLanguage; + +/** + * New first-run onboarding (ADFA-4725): Welcome -> Language -> Permissions -> Set up your library. + * Replaces the legacy SetupActivity in the redesign flow (M3 theme). Notifications / Storage / + * Keep-running only (overlay dropped). Set up your library -> SetupLibraryActivity (Step 1/2). + */ +public class WizardActivity extends AppCompatActivity { + + private int step = 0; + private String langTag = ""; // "" = system + + private TextView title, subtitle, primary; + private View welcome, language, perms, setup; + private TextView notifStatus, storageStatus, batteryStatus; + + private final ActivityResultLauncher permResult = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), r -> render()); + + @Override + protected void onCreate(Bundle b) { + super.onCreate(b); + setContentView(R.layout.activity_k2go_wizard); + title = findViewById(R.id.wiz_title); + subtitle = findViewById(R.id.wiz_subtitle); + primary = findViewById(R.id.wiz_primary); + welcome = findViewById(R.id.wiz_welcome); + language = findViewById(R.id.wiz_language); + perms = findViewById(R.id.wiz_perms); + setup = findViewById(R.id.wiz_setup); + notifStatus = findViewById(R.id.perm_notif_status); + storageStatus = findViewById(R.id.perm_storage_status); + batteryStatus = findViewById(R.id.perm_battery_status); + + // language selection + findViewById(R.id.lang_system).setOnClickListener(v -> pickLang("")); + findViewById(R.id.lang_en).setOnClickListener(v -> pickLang("en")); + findViewById(R.id.lang_es).setOnClickListener(v -> pickLang("es")); + + // permission requests + findViewById(R.id.perm_notif).setOnClickListener(v -> requestNotif()); + findViewById(R.id.perm_storage).setOnClickListener(v -> requestStorage()); + findViewById(R.id.perm_battery).setOnClickListener(v -> requestBattery()); + + // set-up-library choices + findViewById(R.id.setup_download).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, SetupLibraryActivity.class)); + finish(); + }); + findViewById(R.id.setup_copy).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, LibraryActivity.class)); + finish(); + }); + + primary.setOnClickListener(v -> onPrimary()); + render(); + } + + private void pickLang(String tag) { + langTag = tag; + ((ImageView) findViewById(R.id.lang_system_radio)).setImageResource(tag.isEmpty() ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + ((ImageView) findViewById(R.id.lang_en_radio)).setImageResource("en".equals(tag) ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + ((ImageView) findViewById(R.id.lang_es_radio)).setImageResource("es".equals(tag) ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + } + + private void onPrimary() { + if (step == 0) { + step = 1; + } else if (step == 1) { + applyLanguage(); + step = 2; + } else if (step == 2) { + if (allPermsGranted()) step = 3; + } + render(); + } + + private void render() { + welcome.setVisibility(step == 0 ? View.VISIBLE : View.GONE); + language.setVisibility(step == 1 ? View.VISIBLE : View.GONE); + perms.setVisibility(step == 2 ? View.VISIBLE : View.GONE); + setup.setVisibility(step == 3 ? View.VISIBLE : View.GONE); + + switch (step) { + case 0: + title.setText("Knowledge To Go"); + subtitle.setText("A library in your pocket"); + primary.setVisibility(View.VISIBLE); + primary.setText("Get started"); + enable(primary, true); + break; + case 1: + title.setText("Choose language"); + subtitle.setText("Sets app + content language."); + primary.setVisibility(View.VISIBLE); + primary.setText("Next"); + enable(primary, true); + break; + case 2: + title.setText("Allow a few things"); + subtitle.setText(""); + refreshPermStatuses(); + primary.setVisibility(View.VISIBLE); + primary.setText("Continue"); + enable(primary, allPermsGranted()); + break; + default: + title.setText("Set up your library"); + subtitle.setText("How do you want it?"); + primary.setVisibility(View.GONE); + break; + } + } + + private void enable(TextView b, boolean on) { + b.setEnabled(on); + b.setAlpha(on ? 1f : 0.5f); + } + + // --- language --- + private void applyLanguage() { + AppLocaleController.apply(langTag); + String content = langTag.isEmpty() ? ContentLanguage.systemDefault() : langTag; + prefs().edit().putString("selected_lang_minimal", content).apply(); + } + + // --- permissions --- + private void refreshPermStatuses() { + setStatus(notifStatus, hasNotif()); + setStatus(storageStatus, hasStorage()); + setStatus(batteryStatus, hasBattery()); + } + private void setStatus(TextView t, boolean granted) { + t.setText(granted ? "Granted" : "Allow"); + t.setTextColor(ContextCompat.getColor(this, granted ? R.color.k2go_leaf : R.color.k2go_teal)); + } + private boolean allPermsGranted() { + boolean ok = hasStorage() && hasBattery(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ok = ok && hasNotif(); + return ok; + } + private boolean hasNotif() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return NotificationManagerCompat.from(this).areNotificationsEnabled(); + return true; + } + private boolean hasStorage() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return android.os.Environment.isExternalStorageManager(); + return ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == android.content.pm.PackageManager.PERMISSION_GRANTED; + } + private boolean hasBattery() { + 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 void requestNotif() { + if (hasNotif()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Intent i = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + i.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + permResult.launch(i); + } + } + private void requestStorage() { + if (hasStorage()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + Intent i = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + i.addCategory("android.intent.category.DEFAULT"); + i.setData(Uri.parse("package:" + getPackageName())); + permResult.launch(i); + } catch (Exception e) { + permResult.launch(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); + } + } + } + private void requestBattery() { + if (hasBattery()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent i = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + i.setData(Uri.parse("package:" + getPackageName())); + permResult.launch(i); + } + } + + private SharedPreferences prefs() { + return getSharedPreferences(getString(R.string.pref_file_internal), MODE_PRIVATE); + } + private void markComplete() { + prefs().edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); + } +} diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml new file mode 100644 index 00000000..6840288b --- /dev/null +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4e064a7bfa87a8ebeed3d49da65e54fa148fa8e0 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:43:47 -0600 Subject: [PATCH 16/55] ADFA-4725: use the official K2Go logo (splash_logo) in the new onboarding welcome --- controller/app/src/main/res/layout/activity_k2go_wizard.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index 6840288b..b65ec996 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -13,7 +13,8 @@ - + From 26689c69497e767208065751ad442ddfc12f5660 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:49:28 -0600 Subject: [PATCH 17/55] ADFA-4725: extract redesign layout strings to strings_k2go.xml (English, translatable=false) for a later l10n pass --- .../main/res/layout/activity_k2go_wizard.xml | 34 +++++------ .../main/res/layout/fragment_k2go_clone.xml | 22 +++---- .../main/res/layout/fragment_k2go_connect.xml | 18 +++--- .../main/res/layout/fragment_k2go_library.xml | 6 +- .../res/layout/fragment_k2go_setup_step1.xml | 12 ++-- .../res/layout/fragment_k2go_setup_step2a.xml | 16 ++--- .../res/layout/fragment_k2go_setup_step2b.xml | 22 +++---- .../src/main/res/layout/view_k2go_edition.xml | 2 +- .../app/src/main/res/values/strings_k2go.xml | 59 +++++++++++++++++++ 9 files changed, 125 insertions(+), 66 deletions(-) diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index b65ec996..945f5196 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -23,17 +23,17 @@ - + - + - + @@ -43,44 +43,44 @@ - - + + - + - - + + - + - - + + - + + android:text="@string/k2go_perm_nearby_note" android:textColor="@color/k2go_muted" android:textAppearance="?attr/textAppearanceBodySmall" /> + android:gravity="center" android:text="@string/k2go_internet_download" android:background="@drawable/k2go_primary_bg" android:textColor="@color/k2go_on_teal" android:textAppearance="?attr/textAppearanceTitleLarge" android:clickable="true" android:focusable="true" /> + android:text="@string/k2go_internet_download_sub" android:textColor="@color/k2go_muted" android:textAppearance="?attr/textAppearanceBodySmall" /> + android:gravity="center" android:text="@string/k2go_copy_from_phone" android:background="@drawable/k2go_getmore_bg" android:textColor="@color/k2go_teal" android:textAppearance="?attr/textAppearanceTitleLarge" android:clickable="true" android:focusable="true" /> 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 07ed30e8..f7dfbb7a 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -6,16 +6,16 @@ android:orientation="vertical" android:padding="16dp"> + android:text="@string/k2go_tab_clone" android:textAppearance="?attr/textAppearanceHeadlineMedium" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_clone_subtitle" android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_muted" /> + android:gravity="center" android:text="@string/k2go_send" android:background="@drawable/k2go_primary_bg" android:textColor="@color/k2go_on_teal" /> + android:gravity="center" android:text="@string/k2go_receive" android:textColor="@color/k2go_muted" /> @@ -28,18 +28,18 @@ - + - + - + - + @@ -48,13 +48,13 @@ android:layout_marginTop="14dp" android:padding="12dp" android:background="@drawable/k2go_card_bg"> + android:text="@string/k2go_clone_headsup" android:textColor="@color/k2go_ink" android:textAppearance="?attr/textAppearanceBodySmall" /> + android:text="@string/k2go_clone_next_note" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> diff --git a/controller/app/src/main/res/layout/fragment_k2go_connect.xml b/controller/app/src/main/res/layout/fragment_k2go_connect.xml index 79354dc3..04633f00 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_connect.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_connect.xml @@ -6,16 +6,16 @@ android:orientation="vertical" android:padding="16dp"> + android:text="@string/k2go_tab_connect" android:textAppearance="?attr/textAppearanceHeadlineMedium" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_connect_subtitle" android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_muted" /> + android:gravity="center" android:text="@string/k2go_hotspot" android:background="@drawable/k2go_primary_bg" android:textColor="@color/k2go_on_teal" /> + android:gravity="center" android:text="@string/k2go_wifi" android:textColor="@color/k2go_muted" /> @@ -32,21 +32,21 @@ android:textSize="96sp" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_scan_join_hotspot" android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_just_scan" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_teal" /> - + + android:text="@string/k2go_connect_available" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> diff --git a/controller/app/src/main/res/layout/fragment_k2go_library.xml b/controller/app/src/main/res/layout/fragment_k2go_library.xml index 5d30cfb6..f7898842 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_library.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_library.xml @@ -14,7 +14,7 @@ @@ -36,7 +36,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="6dp" - android:text="Starting your library…" + android:text="@string/k2go_starting_library" android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_muted" /> @@ -56,7 +56,7 @@ android:layout_marginTop="16dp" android:padding="16dp" android:gravity="center" - android:text="+ Get more" + android:text="@string/k2go_get_more" android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="@color/k2go_teal" android:background="@drawable/k2go_getmore_bg" diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml index bd938a5b..9d88955f 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml @@ -14,7 +14,7 @@ @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" - android:text="1 System → 2 Content" + android:text="@string/k2go_step1_breadcrumb" android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_teal" /> @@ -30,7 +30,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="Storage" + android:text="@string/k2go_storage" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> @@ -57,7 +57,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="18dp" - android:text="CHOOSE AN EDITION" + android:text="@string/k2go_choose_edition" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_teal" /> @@ -71,7 +71,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="14dp" - android:text="These are apps — the programs that open content. You'll choose the content itself in Step 2." + android:text="@string/k2go_step1_note" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> @@ -82,7 +82,7 @@ android:layout_marginTop="18dp" android:padding="16dp" android:gravity="center" - android:text="Next: choose content" + android:text="@string/k2go_next_choose_content" android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="@color/k2go_on_teal" android:background="@drawable/k2go_primary_bg" diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 74169478..f865444e 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -7,14 +7,14 @@ android:orientation="vertical" android:padding="16dp"> @@ -33,13 +33,13 @@ + android:text="@string/k2go_wikipedia_kiwix" android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_coverage" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> @@ -48,7 +48,7 @@ + android:text="@string/k2go_detail" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> @@ -58,12 +58,12 @@ diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index 98e121d9..70ede39a 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -6,9 +6,9 @@ android:orientation="vertical" android:padding="16dp"> + android:text="@string/k2go_setup_library_title" android:textAppearance="?attr/textAppearanceHeadlineMedium" android:textColor="@color/k2go_ink" /> + android:text="@string/k2go_step2b_breadcrumb" android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_teal" /> @@ -24,7 +24,7 @@ + android:text="@string/k2go_projected_use" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> @@ -37,21 +37,21 @@ - + android:layout_marginEnd="6dp" android:padding="10dp" android:gravity="center" android:text="@string/k2go_cov_popular" android:clickable="true" android:focusable="true" /> + android:padding="10dp" android:gravity="center" android:text="@string/k2go_cov_everything" android:clickable="true" android:focusable="true" /> - + android:layout_marginEnd="6dp" android:padding="10dp" android:gravity="center" android:text="@string/k2go_det_pictures" android:clickable="true" android:focusable="true" /> + android:padding="10dp" android:gravity="center" android:text="@string/k2go_det_text" android:clickable="true" android:focusable="true" /> @@ -60,10 +60,10 @@ diff --git a/controller/app/src/main/res/layout/view_k2go_edition.xml b/controller/app/src/main/res/layout/view_k2go_edition.xml index 2580d14c..d339a3e7 100644 --- a/controller/app/src/main/res/layout/view_k2go_edition.xml +++ b/controller/app/src/main/res/layout/view_k2go_edition.xml @@ -46,7 +46,7 @@ android:paddingEnd="8dp" android:paddingTop="2dp" android:paddingBottom="2dp" - android:text="Recommended" + android:text="@string/k2go_recommended" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_on_teal" android:background="@drawable/k2go_chip_bg" diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 9de46957..d50d40f8 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -1,7 +1,66 @@ + Library Connect Clone Settings + + Get more + 1 System → 2 Content + Allow + App & programs + Available while the app is open. + Back + Books and Maps are included with your edition; Courses is set up after install. + CHOOSE AN EDITION + Content — books, Wikipedia, maps + Copy from a phone + Copy the K2Go app and its library onto another phone + Coverage + Detail + Download library + English + Español + Everything + Get started + Hotspot + Internet download + Just scan — no need to type! + Keep running + Knowledge To Go + Let a nearby device browse your library. + Nearby devices is asked later, only when sharing. + Next + Next: choose content + Next: join the same network, then start. + Notifications + Popular + Recommended + Scan didn\'t work? Use this: + Scan to join my hotspot + Send to a nearby phone + Send + Set up your library + Starting your library… + Storage access + Storage + System default + System ✓ · Content — step 2 of 2 + System ✓ · Content + Text only + The K2Go library on the other phone (if any) is replaced with this one. + These are apps — the programs that open content. You\'ll choose the content itself in Step 2. + They\'re connected — show next › + Total copy + WHAT THEY\'LL RECEIVE + Wi-Fi + Wikipedia (via Kiwix) + With pictures + download progress + projected use + skip battery limits + store & serve content + system + content · 3 GB+ + Receive From 199422c7116b27bb5ed7b5d1ad7dd25b23626edb Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 07:52:22 -0600 Subject: [PATCH 18/55] ADFA-4725/overlay: replace SAW background bring-to-front with a tap-to-return notification to LibraryActivity; drop SYSTEM_ALERT_WINDOW --- controller/app/src/main/AndroidManifest.xml | 1 - .../iiab/controller/AdbPairingReceiver.java | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index 0944d9fb..63efa59f 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -20,7 +20,6 @@ - diff --git a/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java b/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java index cd883236..c481f807 100644 --- a/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java +++ b/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java @@ -19,6 +19,7 @@ public class AdbPairingReceiver extends BroadcastReceiver { public static final String KEY_PIN_REPLY = "key_pin_reply"; public static final int NOTIFICATION_ID = 9401; + public static final int RETURN_NOTIFICATION_ID = 9402; private static final String TAG = "AdbPairingNative"; @@ -73,10 +74,25 @@ private void performNativePairing(Context context, String hostIp, int pairingPor .putBoolean("pairing_just_succeeded", true) .apply(); - // CRITICAL: Bring our app back to the screen automatically! - Intent bringToFront = new Intent(context, MainActivity.class); - bringToFront.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); - context.startActivity(bringToFront); + // ADFA-4725 (overlay decision, Option B): no SAW / no background + // startActivity. Post an OS-sanctioned "tap to return" notification that + // opens the new UI (LibraryActivity). Reliable on restrictive phones. + Intent open = new Intent(context, org.iiab.controller.redesign.LibraryActivity.class); + open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + int piFlags = android.app.PendingIntent.FLAG_UPDATE_CURRENT + | (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M + ? android.app.PendingIntent.FLAG_IMMUTABLE : 0); + android.app.PendingIntent pi = android.app.PendingIntent.getActivity(context, 0, open, piFlags); + NotificationManager nmReturn = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (nmReturn != null) { + NotificationCompat.Builder rb = new NotificationCompat.Builder(context, "adb_pairing_channel") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("Paired") + .setContentText("Tap to return to K2Go") + .setAutoCancel(true) + .setContentIntent(pi); + nmReturn.notify(RETURN_NOTIFICATION_ID, rb.build()); + } } else { Log.e(TAG, "Native pairing failed."); From 40716096fa2b1be984becaa6084f007c22985967 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 08:00:57 -0600 Subject: [PATCH 19/55] ADFA-4725: pin the onboarding CTA to the bottom (consistent across steps, matches the wizard diagram) --- controller/app/src/main/res/layout/activity_k2go_wizard.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index 945f5196..bc559495 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -79,6 +79,10 @@ android:gravity="center" android:text="@string/k2go_copy_from_phone" android:background="@drawable/k2go_getmore_bg" android:textColor="@color/k2go_teal" android:textAppearance="?attr/textAppearanceTitleLarge" android:clickable="true" android:focusable="true" /> + + + From 9076d40c64a361b395964b4ad4db4ce61118e274 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 08:05:35 -0600 Subject: [PATCH 20/55] =?UTF-8?q?ADFA-4725:=20onboarding=20Welcome=20?= =?UTF-8?q?=E2=80=94=20logo=20on=20top,=20title+subtitle=20centered=20belo?= =?UTF-8?q?w=20(matches=20the=20wizard=20diagram)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/iiab/controller/redesign/WizardActivity.java | 4 ++-- controller/app/src/main/res/layout/activity_k2go_wizard.xml | 6 ++++++ controller/app/src/main/res/values/strings_k2go.xml | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java index bda20571..dacae8e0 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java @@ -102,11 +102,11 @@ private void render() { language.setVisibility(step == 1 ? View.VISIBLE : View.GONE); perms.setVisibility(step == 2 ? View.VISIBLE : View.GONE); setup.setVisibility(step == 3 ? View.VISIBLE : View.GONE); + title.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + subtitle.setVisibility(step == 0 ? View.GONE : View.VISIBLE); switch (step) { case 0: - title.setText("Knowledge To Go"); - subtitle.setText("A library in your pocket"); primary.setVisibility(View.VISIBLE); primary.setText("Get started"); enable(primary, true); diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index bc559495..732a7c5d 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -15,6 +15,12 @@ android:orientation="vertical" android:layout_marginTop="24dp" android:gravity="center"> + + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index d50d40f8..5b59108e 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -63,4 +63,5 @@ store & serve content system + content · 3 GB+ Receive + A library in your pocket From 28d36acf1db30c2938ee65a1ad182ef4cdf54535 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 08:38:10 -0600 Subject: [PATCH 21/55] =?UTF-8?q?ADFA-4725:=20onboarding=20Welcome=20?= =?UTF-8?q?=E2=80=94=20nudge=20the=20logo/title=20block=20down=20(top=20sp?= =?UTF-8?q?acer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/app/src/main/res/layout/activity_k2go_wizard.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index 732a7c5d..f1e254e5 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -13,6 +13,7 @@ + Date: Fri, 17 Jul 2026 08:40:47 -0600 Subject: [PATCH 22/55] =?UTF-8?q?ADFA-4725:=20onboarding=20=E2=80=94=20add?= =?UTF-8?q?=20the=20'<=20Back'=20link=20on=20Language/Permissions/Set-up?= =?UTF-8?q?=20steps=20+=20step-through=20system=20back=20(matches=20update?= =?UTF-8?q?d=20wizard=20diagram)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iiab/controller/redesign/WizardActivity.java | 15 ++++++++++++++- .../src/main/res/layout/activity_k2go_wizard.xml | 4 ++++ .../app/src/main/res/values/strings_k2go.xml | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java index dacae8e0..ba4ea6ac 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java @@ -31,7 +31,7 @@ public class WizardActivity extends AppCompatActivity { private String langTag = ""; // "" = system private TextView title, subtitle, primary; - private View welcome, language, perms, setup; + private View welcome, language, perms, setup, back; private TextView notifStatus, storageStatus, batteryStatus; private final ActivityResultLauncher permResult = @@ -48,6 +48,7 @@ protected void onCreate(Bundle b) { language = findViewById(R.id.wiz_language); perms = findViewById(R.id.wiz_perms); setup = findViewById(R.id.wiz_setup); + back = findViewById(R.id.wiz_back); notifStatus = findViewById(R.id.perm_notif_status); storageStatus = findViewById(R.id.perm_storage_status); batteryStatus = findViewById(R.id.perm_battery_status); @@ -75,6 +76,7 @@ protected void onCreate(Bundle b) { }); primary.setOnClickListener(v -> onPrimary()); + back.setOnClickListener(v -> goBack()); render(); } @@ -97,6 +99,16 @@ private void onPrimary() { render(); } + private void goBack() { + if (step > 0) { step--; render(); } + } + + @Override + public void onBackPressed() { + if (step > 0) { step--; render(); } + else super.onBackPressed(); + } + private void render() { welcome.setVisibility(step == 0 ? View.VISIBLE : View.GONE); language.setVisibility(step == 1 ? View.VISIBLE : View.GONE); @@ -104,6 +116,7 @@ private void render() { setup.setVisibility(step == 3 ? View.VISIBLE : View.GONE); title.setVisibility(step == 0 ? View.GONE : View.VISIBLE); subtitle.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + back.setVisibility(step >= 1 ? View.VISIBLE : View.GONE); switch (step) { case 0: diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index f1e254e5..a5b7de1f 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -93,5 +93,9 @@ + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 5b59108e..b38eccd8 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -64,4 +64,5 @@ system + content · 3 GB+ Receive A library in your pocket + ‹ Back From 5a128bb0b408ad03fc3aae8119e039e04095e419 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 08:50:59 -0600 Subject: [PATCH 23/55] ADFA-4725: fix invisible storage bars (setBackgroundColor) + Step 1 '< Back' link and bottom-anchored CTA (consistent with wizard) --- .../org/iiab/controller/redesign/Step1SystemFragment.java | 3 ++- .../org/iiab/controller/redesign/Step2OptionAFragment.java | 2 +- .../org/iiab/controller/redesign/Step2OptionBFragment.java | 2 +- .../app/src/main/res/layout/fragment_k2go_setup_step1.xml | 6 ++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java index 7b5ec7e8..f38b495f 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -78,6 +78,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } }); + root.findViewById(R.id.k2go_step1_back).setOnClickListener(v -> requireActivity().finish()); select(selected); return root; } @@ -129,6 +130,6 @@ private void setWeight(View v, float w) { } private void tint(View v, int colorRes) { - v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); } } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index b089408b..2b4b9c3d 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -176,6 +176,6 @@ private void setW(View v, float w) { lp.weight = w; v.setLayoutParams(lp); } private void tintBg(View v, int colorRes) { - v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); } } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 9cab6a4b..404fa7f3 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -175,6 +175,6 @@ private void setW(View v, float w) { lp.weight = w; v.setLayoutParams(lp); } private void tintBg(View v, int colorRes) { - v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); } } diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml index 9d88955f..04f7e99f 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml @@ -75,6 +75,8 @@ android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> + + + From 3b5169fc7f869df4143ff4fa355a2ec069847789 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 08:53:22 -0600 Subject: [PATCH 24/55] =?UTF-8?q?ADFA-4725:=20fix=20crash=20on=20Library?= =?UTF-8?q?=20open=20=E2=80=94=20feed=20Lottie=20the=20app=20Atkinson=20fo?= =?UTF-8?q?nt=20via=20FontAssetDelegate=20(OPEN/CLOSED=20sign=20text=20lay?= =?UTF-8?q?er)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/iiab/controller/redesign/LibraryActivity.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 9c9f4d51..b88bae9f 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -64,6 +64,17 @@ protected void onCreate(Bundle savedInstanceState) { } bootGate = findViewById(R.id.k2go_boot_gate); + // The Lottie has a text layer (OPEN/CLOSED sign) referencing "Atkinson Hyperlegible". + // Without this delegate Lottie looks for assets/fonts/Atkinson Hyperlegible.ttf and + // crashes (Font asset not found). Feed it the app's Atkinson font resource instead. + bootGate.setFontAssetDelegate(new com.airbnb.lottie.FontAssetDelegate() { + @Override + public android.graphics.Typeface fetchFont(String fontFamily) { + android.graphics.Typeface tf = androidx.core.content.res.ResourcesCompat.getFont( + LibraryActivity.this, R.font.atkinson_hyperlegible); + return tf != null ? tf : android.graphics.Typeface.DEFAULT; + } + }); if (!reduceMotion()) { bootGate.setAnimation(R.raw.library_animation); bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); From 0a75594c334ae97363f85e391172d565085baa10 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 09:04:04 -0600 Subject: [PATCH 25/55] =?UTF-8?q?ADFA-4725:=20Step=202=20(A=20&=20B)=20?= =?UTF-8?q?=E2=80=94=20bottom-anchored=20primary=20CTA=20+=20'<=20Back'=20?= =?UTF-8?q?link,=20consistent=20with=20Step=201=20/=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/iiab/controller/redesign/Step2OptionAFragment.java | 3 +++ .../org/iiab/controller/redesign/Step2OptionBFragment.java | 5 ++++- .../app/src/main/res/layout/fragment_k2go_setup_step2a.xml | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index 2b4b9c3d..8e3b5a08 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -78,6 +78,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c detPictures.setOnClickListener(x -> { pictures = true; refresh(); }); detText.setOnClickListener(x -> { pictures = false; refresh(); }); download.setOnClickListener(x -> startDownload()); + v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> { + if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); + }); // Real Kiwix catalog for this language (cached; network only if stale). InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 404fa7f3..951e9d16 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -84,7 +84,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c covEvery.setOnClickListener(x -> { everything = true; persist(); refreshProjection(); }); detPic.setOnClickListener(x -> { pictures = true; persist(); refreshProjection(); }); detTxt.setOnClickListener(x -> { pictures = false; persist(); refreshProjection(); }); - btnBack.setOnClickListener(x -> { if (step > 0) { step--; render(); } }); + btnBack.setOnClickListener(x -> { + if (step > 0) { step--; render(); } + else if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); + }); btnNext.setOnClickListener(x -> { if (step < 4) { step++; render(); } else startDownload(); }); AbFlip.attach(caption != null ? v.findViewById(R.id.k2go_step2_title) : v, () -> { if (act() != null) act().flipAbTest(); }); diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index f865444e..20e9a4d0 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -61,10 +61,15 @@ android:text="@string/k2go_step2a_note" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> + + From 8fcd7517cc5cf8ed40ce5956764c4e0bdcc637d2 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 09:04:45 -0600 Subject: [PATCH 26/55] =?UTF-8?q?ADFA-4725:=20Step=202=20Option=20B=20?= =?UTF-8?q?=E2=80=94=20stack=20Next=20(full-width)=20+=20'<=20Back'=20link?= =?UTF-8?q?=20(consistent=20bottom=20pattern)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../res/layout/fragment_k2go_setup_step2b.xml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index 70ede39a..c7c4460d 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -58,13 +58,12 @@ - - - - + + + From 3d7ec948678db0f63f385cb6db819d5f25f974a1 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:11:55 -0600 Subject: [PATCH 27/55] =?UTF-8?q?ADFA-4725:=20Step=202=20Option=20A=20per?= =?UTF-8?q?=20design=20=E2=80=94=20collapsible=20Wikipedia/Books/Maps=20+?= =?UTF-8?q?=20disabled=20Courses,=20per-row=20Skip/Add,=20live=20bar+total?= =?UTF-8?q?,=20skip-all=3Dsystem=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redesign/Step2OptionAFragment.java | 224 ++++++++++-------- .../res/layout/fragment_k2go_setup_step2a.xml | 108 +++++---- .../app/src/main/res/values/strings_k2go.xml | 12 + 3 files changed, 203 insertions(+), 141 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index 8e3b5a08..d3d4f4d9 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -22,123 +23,161 @@ import org.json.JSONObject; /** - * Step 2 Content — Option A (Expandable + storage Bar). Wikipedia coverage/detail compose a - * Kiwix variant with real catalog sizes; the bar + Download total come from InstallationPlanner; - * Download starts the real InstallService (tier + companion + lang + variant). + * Step 2 Content — Option A (Expandable + Bar). Collapsible sources: Wikipedia (real Kiwix + * variant, skippable), Books (Coming soon, cosmetic skip), Maps (fixed 0.2 GB), Courses + * (disabled). Bar + total reflect the picks; Download drives InstallService (companion + + * Wikipedia variant). Skip everything -> just the system. */ public class Step2OptionAFragment extends Fragment { - private boolean everything = false; // false = Popular - private boolean pictures = true; // true = With pictures - private String lang; - private JSONObject langData; // catalog entry for the language (nullable) + private static final double BOOKS_GB = 0.8, MAPS_GB = 0.2, WIKI_FALLBACK = 4.6; - private View barUsed, barSystem, barPicks, barFree; + private boolean everything = false, pictures = true; + private boolean wikiInc = true, booksInc = true; + private final boolean mapsInc = true; // Maps content is fixed for now + private String lang; + private JSONObject langData; + private double osGb = 1.4; + + private CheckBox wikiCheck, booksCheck, mapsCheck; + private View wikiBody, booksBody, mapsBody; + private TextView wikiChevron, booksChevron, mapsChevron, wikiSkip, booksSkip; + private View wikiCard, booksCard; + private TextView wikiSize, booksSize, legend, download; + private TextView covPop, covEvery, detPic, detTxt; private LinearLayout bar; - private TextView legend, wikiSize, download; - private TextView covPopular, covEverything, detPictures, detText; + private View bU, bS, bP, bF; - private InstallationPlanner.Tier tier() { - return (getActivity() instanceof SetupLibraryActivity) - ? ((SetupLibraryActivity) getActivity()).getSelectedTier() - : InstallationPlanner.Tier.STANDARD; + private SetupLibraryActivity act() { + return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; + } + private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } + private String popularBase() { return (langData != null && langData.has("top1m_maxi")) ? "top1m" : "top"; } + private String coverageBase() { return everything ? "all" : popularBase(); } + private String variantKey() { return coverageBase() + "_" + (pictures ? "maxi" : "nopic"); } + private double sizeOf(String variant) { + if (langData == null) return -1; + JSONObject o = langData.optJSONObject(variant); + return o == null ? -1 : o.optDouble("size", -1); } + private double wikiSizeGb() { + double s = sizeOf(variantKey()); + return s >= 0 ? s : WIKI_FALLBACK; + } + private static String gb(double s) { return s >= 0 ? String.format(Locale.US, "%.1fG", s) : "—"; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { View v = inflater.inflate(R.layout.fragment_k2go_setup_step2a, c, false); lang = ContentLanguage.systemDefault(); - if (getActivity() instanceof SetupLibraryActivity) { - everything = ((SetupLibraryActivity) getActivity()).isEverything(); - pictures = ((SetupLibraryActivity) getActivity()).isPictures(); - } - AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { - if (getActivity() instanceof SetupLibraryActivity) ((SetupLibraryActivity) getActivity()).flipAbTest(); - }); + if (act() != null) { everything = act().isEverything(); pictures = act().isPictures(); } + bar = v.findViewById(R.id.k2go_bar); - barUsed = v.findViewById(R.id.k2go_bar_used); - barSystem = v.findViewById(R.id.k2go_bar_system); - barPicks = v.findViewById(R.id.k2go_bar_picks); - barFree = v.findViewById(R.id.k2go_bar_free); + bU = v.findViewById(R.id.k2go_bar_used); bS = v.findViewById(R.id.k2go_bar_system); + bP = v.findViewById(R.id.k2go_bar_picks); bF = v.findViewById(R.id.k2go_bar_free); legend = v.findViewById(R.id.k2go_legend); - wikiSize = v.findViewById(R.id.k2go_wiki_size); download = v.findViewById(R.id.k2go_download); - covPopular = v.findViewById(R.id.k2go_cov_popular); - covEverything = v.findViewById(R.id.k2go_cov_everything); - detPictures = v.findViewById(R.id.k2go_det_pictures); - detText = v.findViewById(R.id.k2go_det_text); - tintBg(barUsed, R.color.k2go_muted); - tintBg(barSystem, R.color.k2go_teal); - tintBg(barPicks, R.color.k2go_leaf); - tintBg(barFree, R.color.k2go_hairline); - - covPopular.setOnClickListener(x -> { everything = false; refresh(); }); - covEverything.setOnClickListener(x -> { everything = true; refresh(); }); - detPictures.setOnClickListener(x -> { pictures = true; refresh(); }); - detText.setOnClickListener(x -> { pictures = false; refresh(); }); + wikiCheck = v.findViewById(R.id.k2go_wiki_check); booksCheck = v.findViewById(R.id.k2go_books_check); mapsCheck = v.findViewById(R.id.k2go_maps_check); + wikiBody = v.findViewById(R.id.k2go_wiki_body); booksBody = v.findViewById(R.id.k2go_books_body); mapsBody = v.findViewById(R.id.k2go_maps_body); + wikiChevron = v.findViewById(R.id.k2go_wiki_chevron); booksChevron = v.findViewById(R.id.k2go_books_chevron); mapsChevron = v.findViewById(R.id.k2go_maps_chevron); + wikiSkip = v.findViewById(R.id.k2go_wiki_skip); booksSkip = v.findViewById(R.id.k2go_books_skip); + wikiCard = v.findViewById(R.id.k2go_wiki_card); booksCard = v.findViewById(R.id.k2go_books_card); + wikiSize = v.findViewById(R.id.k2go_wiki_size); booksSize = v.findViewById(R.id.k2go_books_size); + covPop = v.findViewById(R.id.k2go_cov_popular); covEvery = v.findViewById(R.id.k2go_cov_everything); + detPic = v.findViewById(R.id.k2go_det_pictures); detTxt = v.findViewById(R.id.k2go_det_text); + colorSeg(bU, R.color.k2go_muted); colorSeg(bS, R.color.k2go_teal); colorSeg(bP, R.color.k2go_leaf); colorSeg(bF, R.color.k2go_hairline); + + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); + + v.findViewById(R.id.k2go_wiki_header).setOnClickListener(x -> toggle(wikiBody, wikiChevron)); + v.findViewById(R.id.k2go_books_header).setOnClickListener(x -> toggle(booksBody, booksChevron)); + v.findViewById(R.id.k2go_maps_header).setOnClickListener(x -> toggle(mapsBody, mapsChevron)); + + wikiCheck.setOnClickListener(x -> setWiki(wikiCheck.isChecked())); + wikiSkip.setOnClickListener(x -> setWiki(!wikiInc)); + booksCheck.setOnClickListener(x -> setBooks(booksCheck.isChecked())); + booksSkip.setOnClickListener(x -> setBooks(!booksInc)); + mapsCheck.setOnClickListener(x -> mapsCheck.setChecked(true)); // fixed + + covPop.setOnClickListener(x -> { everything = false; persist(); refresh(); }); + covEvery.setOnClickListener(x -> { if (covEvery.isEnabled()) { everything = true; persist(); refresh(); } }); + detPic.setOnClickListener(x -> { pictures = true; persist(); refresh(); }); + detTxt.setOnClickListener(x -> { pictures = false; persist(); refresh(); }); + download.setOnClickListener(x -> startDownload()); v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> { if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); }); - // Real Kiwix catalog for this language (cached; network only if stale). + // system size (no network) and Kiwix catalog (cached) both drive the numbers. + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), false, lang, null, + new InstallationPlanner.PlanResultListener() { + @Override public void onCalculated(InstallationPlanner.StorageProjection p) { if (isAdded()) { osGb = p.osSize; refresh(); } } + @Override public void onError(String e) { } + }); InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { - @Override - public void onReady(JSONObject catalog) { + @Override public void onReady(JSONObject catalog) { if (!isAdded()) return; langData = catalog.optJSONObject(lang); if (langData == null) langData = catalog.optJSONObject("en"); refresh(); } - @Override - public void onError(String error) { /* keep "—"; Download still resolves the variant */ } + @Override public void onError(String e) { } }); + refresh(); return v; } - private String popularBase() { - return (langData != null && langData.has("top1m_maxi")) ? "top1m" : "top"; + private void persist() { if (act() != null) { act().setEverything(everything); act().setPictures(pictures); } } + + private void toggle(View body, TextView chevron) { + boolean show = body.getVisibility() != View.VISIBLE; + body.setVisibility(show ? View.VISIBLE : View.GONE); + chevron.setText(show ? "▾" : "▸"); } - private String coverageBase() { return everything ? "all" : popularBase(); } - private String variantKey() { return coverageBase() + "_" + (pictures ? "maxi" : "nopic"); } - private double sizeOf(String variant) { - if (langData == null) return -1; - JSONObject o = langData.optJSONObject(variant); - return o == null ? -1 : o.optDouble("size", -1); + private void setWiki(boolean inc) { + wikiInc = inc; + wikiCheck.setChecked(inc); + wikiCard.setAlpha(inc ? 1f : 0.55f); + wikiSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); + if (!inc) { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } + refresh(); + } + private void setBooks(boolean inc) { + booksInc = inc; + booksCheck.setChecked(inc); + booksCard.setAlpha(inc ? 1f : 0.55f); + booksSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); + if (!inc) { booksBody.setVisibility(View.GONE); booksChevron.setText("▸"); } + refresh(); } - private static String gb(double s) { return s >= 0 ? String.format(Locale.US, "%.1fG", s) : "—"; } private void refresh() { - if (getActivity() instanceof SetupLibraryActivity) { - ((SetupLibraryActivity) getActivity()).setEverything(everything); - ((SetupLibraryActivity) getActivity()).setPictures(pictures); - } - // toggle labels with live sizes - label(covPopular, "Popular", sizeOf(popularBase() + "_" + (pictures ? "maxi" : "nopic")), !everything); - label(covEverything, "Everything", sizeOf("all_" + (pictures ? "maxi" : "nopic")), everything); - label(detPictures, "With pictures", sizeOf(coverageBase() + "_maxi"), pictures); - label(detText, "Text only", sizeOf(coverageBase() + "_nopic"), !pictures); - - InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variantKey(), - new InstallationPlanner.PlanResultListener() { - @Override - public void onCalculated(InstallationPlanner.StorageProjection p) { - if (!isAdded()) return; - wikiSize.setText(gb(p.kiwixSize)); - applyBar(p.osSize, p.mapsSize + p.kiwixSize); - download.setText(String.format(Locale.US, "Download library · %.1f GB", p.totalSize)); - } - @Override public void onError(String e) { } - }); + double free = StorageInfo.freeGb(); + // Wikipedia variant labels + "Everything won't fit" guard + boolean everyFits = sizeOf("all_" + (pictures ? "maxi" : "nopic")) < 0 + || sizeOf("all_" + (pictures ? "maxi" : "nopic")) <= (free - osGb); + label(covPop, "Popular", sizeOf(popularBase() + "_" + (pictures ? "maxi" : "nopic")), !everything, true); + label(covEvery, "Everything", sizeOf("all_" + (pictures ? "maxi" : "nopic")), everything, everyFits); + label(detPic, "With pictures", sizeOf(coverageBase() + "_maxi"), pictures, true); + label(detTxt, "Text only", sizeOf(coverageBase() + "_nopic"), !pictures, true); + wikiSize.setText(wikiInc ? gb(wikiSizeGb()) : getString(R.string.k2go_skipped)); + booksSize.setText(booksInc ? "0.8 GB" : getString(R.string.k2go_skipped)); + + double picks = (wikiInc ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0) + (mapsInc ? MAPS_GB : 0); + applyBar(osGb, picks); + download.setText(String.format(Locale.US, "Download library · %.1f GB", osGb + picks)); } - private void label(TextView t, String name, double size, boolean on) { + private void label(TextView t, String name, double size, boolean on, boolean enabled) { t.setText(name + " " + gb(size)); - t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); - t.setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_ink)); + t.setEnabled(enabled); + t.setAlpha(enabled ? 1f : 0.4f); + t.setBackgroundResource(on && enabled ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + t.setTextColor(ContextCompat.getColor(requireContext(), on && enabled ? R.color.k2go_on_teal : R.color.k2go_ink)); } private void applyBar(double systemGb, double picksGb) { @@ -147,38 +186,27 @@ private void applyBar(double systemGb, double picksGb) { double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb - picksGb); if (total <= 0) total = used + systemGb + picksGb + freeAfter + 0.01; bar.setWeightSum((float) total); - setW(barUsed, (float) used); - setW(barSystem, (float) systemGb); - setW(barPicks, (float) picksGb); - setW(barFree, (float) freeAfter); - legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Picks %.1f · Free %.1f", - used, systemGb, picksGb, freeAfter)); + setW(bU, (float) used); setW(bS, (float) systemGb); setW(bP, (float) picksGb); setW(bF, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Your picks %.1f · Free %.1f", used, systemGb, picksGb, freeAfter)); } private void startDownload() { + boolean companion = wikiInc || booksInc || mapsInc; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); i.putExtra(InstallService.EXTRA_TIER, tier().name()); - i.putExtra(InstallService.EXTRA_COMPANION, true); + i.putExtra(InstallService.EXTRA_COMPANION, companion); i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); - i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variantKey()); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, wikiInc ? variantKey() : null); i.putExtra(InstallService.EXTRA_REINSTALL, false); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - requireContext().startForegroundService(i); - } else { - requireContext().startService(i); - } - Toast.makeText(requireContext(), "Downloading your library…", Toast.LENGTH_LONG).show(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); + else requireContext().startService(i); + Toast.makeText(requireContext(), companion ? "Downloading your library…" : "Installing the system…", Toast.LENGTH_LONG).show(); startActivity(new Intent(requireContext(), LibraryActivity.class)); requireActivity().finish(); } - private void setW(View v, float w) { - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); - lp.weight = w; v.setLayoutParams(lp); - } - private void tintBg(View v, int colorRes) { - v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); - } + private void setW(View v, float w) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); lp.weight = w; v.setLayoutParams(lp); } + private void colorSeg(View v, int colorRes) { v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); } } diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 20e9a4d0..01f3f30b 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -2,22 +2,17 @@ - - + android:text="@string/k2go_setup_library_title" android:textAppearance="?attr/textAppearanceHeadlineMedium" android:textColor="@color/k2go_ink" /> + - - + + @@ -26,50 +21,77 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + android:text="@string/k2go_step2a_hint" android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="@color/k2go_muted" /> + android:layout_marginTop="12dp" android:padding="16dp" android:gravity="center" android:text="@string/k2go_download_library" + android:textColor="@color/k2go_on_teal" android:background="@drawable/k2go_primary_bg" android:textAppearance="?attr/textAppearanceTitleLarge" android:clickable="true" android:focusable="true" /> + android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="@color/k2go_teal" android:clickable="true" android:focusable="true" /> diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index b38eccd8..d7bac485 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -65,4 +65,16 @@ Receive A library in your pocket ‹ Back + Wikipedia + Books + Maps + Courses + Skip + Add + skipped + Coming soon — no content to choose yet. + Offline world base map. Included with the map (fixed for now). + Kiwix presets exist; Kolibri can\'t be preset yet — set up on the device after install. + added after install + Tap a source to Add or Skip it. Skip all = just the system. From 628ec785eaf963a93bc74459834ccfe9a6d4b1db Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:19:48 -0600 Subject: [PATCH 28/55] ADFA-4725: Step 2 Option B uses the arc gauge (reuse MultiResourceGaugeView) instead of a horizontal bar --- .../main/res/layout/fragment_k2go_setup_step2b.xml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index c7c4460d..6b6c9a67 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -21,17 +21,8 @@ - - - - - - - - + From 20a300128becf002d32a70484c497fcb372a2265 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:21:01 -0600 Subject: [PATCH 29/55] ADFA-4725: fix Step 2 Option B fragment to use the arc gauge (removes stale bar refs; compiles) --- .../redesign/Step2OptionBFragment.java | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 951e9d16..74c80588 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -1,29 +1,30 @@ package org.iiab.controller.redesign; import android.content.Intent; -import android.content.res.ColorStateList; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.MultiResourceGaugeView; import org.iiab.controller.R; import org.iiab.controller.SystemStateEvaluator; import org.iiab.controller.applang.data.ContentLanguage; import org.iiab.controller.install.presentation.InstallService; /** - * Step 2 Content — Option B (5-step map + Gauge). Guided: one source per step, a projected-use - * gauge (real total from InstallationPlanner), Wikipedia coverage/detail on step 1, Download on - * Review. Same install payload as Option A; picks are shared via SetupLibraryActivity. + * Step 2 Content — Option B (5-step map + arc Gauge). Guided: one source per step, a + * projected-use gauge (reuses the old UI's MultiResourceGaugeView), Wikipedia coverage/detail + * on step 1, Download on Review. Same install payload as Option A; picks shared via the Activity. */ public class Step2OptionBFragment extends Fragment { @@ -38,11 +39,10 @@ public class Step2OptionBFragment extends Fragment { private double lastTotal = 0; private final TextView[] dots = new TextView[5]; - private TextView caption, gaugeTotal, legend, btnBack, btnNext, info; + private TextView caption, legend, btnBack, btnNext, info; + private MultiResourceGaugeView gauge; private TextView covPop, covEvery, detPic, detTxt; private View wikiBlock; - private LinearLayout bar; - private View bU, bS, bP, bF; private SetupLibraryActivity act() { return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; @@ -60,25 +60,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c int[] ids = {R.id.k2go_sd0, R.id.k2go_sd1, R.id.k2go_sd2, R.id.k2go_sd3, R.id.k2go_sd4}; for (int i = 0; i < 5; i++) dots[i] = v.findViewById(ids[i]); caption = v.findViewById(R.id.k2go_step_caption); - gaugeTotal = v.findViewById(R.id.k2go_gauge_total); + gauge = v.findViewById(R.id.k2go_gauge); legend = v.findViewById(R.id.k2go_legend); info = v.findViewById(R.id.k2go_info_text); btnBack = v.findViewById(R.id.k2go_btn_back); btnNext = v.findViewById(R.id.k2go_btn_next); wikiBlock = v.findViewById(R.id.k2go_wiki_block); - bar = v.findViewById(R.id.k2go_bar); - bU = v.findViewById(R.id.k2go_bar_used); - bS = v.findViewById(R.id.k2go_bar_system); - bP = v.findViewById(R.id.k2go_bar_picks); - bF = v.findViewById(R.id.k2go_bar_free); covPop = v.findViewById(R.id.k2go_cov_popular); covEvery = v.findViewById(R.id.k2go_cov_everything); detPic = v.findViewById(R.id.k2go_det_pictures); detTxt = v.findViewById(R.id.k2go_det_text); - tintBg(bU, R.color.k2go_muted); - tintBg(bS, R.color.k2go_teal); - tintBg(bP, R.color.k2go_leaf); - tintBg(bF, R.color.k2go_hairline); covPop.setOnClickListener(x -> { everything = false; persist(); refreshProjection(); }); covEvery.setOnClickListener(x -> { everything = true; persist(); refreshProjection(); }); @@ -89,7 +80,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c else if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); }); btnNext.setOnClickListener(x -> { if (step < 4) { step++; render(); } else startDownload(); }); - AbFlip.attach(caption != null ? v.findViewById(R.id.k2go_step2_title) : v, () -> { if (act() != null) act().flipAbTest(); }); + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); render(); refreshProjection(); @@ -138,21 +129,26 @@ private void refreshProjection() { public void onCalculated(InstallationPlanner.StorageProjection p) { if (!isAdded()) return; lastTotal = p.totalSize; - gaugeTotal.setText(String.format(Locale.US, "%.1f GB", p.totalSize)); - applyBar(p.osSize, p.mapsSize + p.kiwixSize); + updateGauge(p.osSize, p.mapsSize + p.kiwixSize); updateNextLabel(); } @Override public void onError(String e) { } }); } - private void applyBar(double systemGb, double picksGb) { + private void updateGauge(double systemGb, double picksGb) { double total = StorageInfo.totalGb(); double used = StorageInfo.usedGb(); - double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb - picksGb); - if (total <= 0) total = used + systemGb + picksGb + freeAfter + 0.01; - bar.setWeightSum((float) total); - setW(bU, (float) used); setW(bS, (float) systemGb); setW(bP, (float) picksGb); setW(bF, (float) freeAfter); + if (total <= 0) total = used + systemGb + picksGb + 0.01; + double freeAfter = Math.max(0, total - used - systemGb - picksGb); + List segs = new ArrayList<>(); + segs.add(new MultiResourceGaugeView.Segment((float) (used / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_muted))); + segs.add(new MultiResourceGaugeView.Segment((float) (systemGb / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_teal))); + segs.add(new MultiResourceGaugeView.Segment((float) (picksGb / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_leaf))); + if (gauge != null) { + gauge.updateData(segs, String.format(Locale.US, "%.1fG", used + systemGb + picksGb), + ContextCompat.getColor(requireContext(), R.color.k2go_ink), "projected use", ""); + } legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Picks %.1f · Free %.1f", used, systemGb, picksGb, freeAfter)); } @@ -172,12 +168,4 @@ private void startDownload() { startActivity(new Intent(requireContext(), LibraryActivity.class)); requireActivity().finish(); } - - private void setW(View v, float w) { - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); - lp.weight = w; v.setLayoutParams(lp); - } - private void tintBg(View v, int colorRes) { - v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); - } } From f89192451592432b7b72d2c0065b6335ddc5af40 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:26:18 -0600 Subject: [PATCH 30/55] =?UTF-8?q?ADFA-4725:=20Step=202=20fixes=20=E2=80=94?= =?UTF-8?q?=20Books=200=20GB=20(coming=20soon),=20Step-2=20breadcrumb=20ke?= =?UTF-8?q?eps=20numbers,=20draw=20(i)=20info=20badges=20on=20editions=20+?= =?UTF-8?q?=20sources=20+=20coverage/detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/iiab/controller/redesign/Step2OptionAFragment.java | 4 ++-- .../org/iiab/controller/redesign/Step2OptionBFragment.java | 2 +- controller/app/src/main/res/drawable/k2go_info_circle.xml | 4 ++++ controller/app/src/main/res/layout/view_k2go_edition.xml | 1 + controller/app/src/main/res/values/strings_k2go.xml | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 controller/app/src/main/res/drawable/k2go_info_circle.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index d3d4f4d9..bc3a255b 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -30,7 +30,7 @@ */ public class Step2OptionAFragment extends Fragment { - private static final double BOOKS_GB = 0.8, MAPS_GB = 0.2, WIKI_FALLBACK = 4.6; + private static final double BOOKS_GB = 0.0, MAPS_GB = 0.2, WIKI_FALLBACK = 4.6; private boolean everything = false, pictures = true; private boolean wikiInc = true, booksInc = true; @@ -165,7 +165,7 @@ private void refresh() { label(detPic, "With pictures", sizeOf(coverageBase() + "_maxi"), pictures, true); label(detTxt, "Text only", sizeOf(coverageBase() + "_nopic"), !pictures, true); wikiSize.setText(wikiInc ? gb(wikiSizeGb()) : getString(R.string.k2go_skipped)); - booksSize.setText(booksInc ? "0.8 GB" : getString(R.string.k2go_skipped)); + booksSize.setText("0 GB"); double picks = (wikiInc ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0) + (mapsInc ? MAPS_GB : 0); applyBar(osGb, picks); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 74c80588..816c8fd5 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -30,7 +30,7 @@ public class Step2OptionBFragment extends Fragment { private static final String[] NAMES = {"Wikipedia", "Books", "Maps", "Courses", "Review"}; private static final String[] INFO = { - "", "Books — included with your edition.", "Maps — included with your edition.", + "", "Books — coming soon (no content manager yet · 0 GB).", "Maps — included with your edition.", "Courses — set up on the device after install.", "Review your library, then download."}; private int step = 0; diff --git a/controller/app/src/main/res/drawable/k2go_info_circle.xml b/controller/app/src/main/res/drawable/k2go_info_circle.xml new file mode 100644 index 00000000..27f7bf0a --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_info_circle.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/layout/view_k2go_edition.xml b/controller/app/src/main/res/layout/view_k2go_edition.xml index d339a3e7..7bd48ad1 100644 --- a/controller/app/src/main/res/layout/view_k2go_edition.xml +++ b/controller/app/src/main/res/layout/view_k2go_edition.xml @@ -36,6 +36,7 @@ android:layout_height="wrap_content" android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="@color/k2go_ink" /> + Storage System default System ✓ · Content — step 2 of 2 - System ✓ · Content + 1 ✓ System → 2 Content Text only The K2Go library on the other phone (if any) is replaced with this one. These are apps — the programs that open content. You\'ll choose the content itself in Step 2. From 1f42a38d37983a2f211ce99bf82571de53018688 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:27:01 -0600 Subject: [PATCH 31/55] ADFA-4725: draw (i) info badges in Step 2 Option A (Wikipedia/Books/Maps + Coverage/Detail) --- .../res/layout/fragment_k2go_setup_step2a.xml | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 01f3f30b..92196fc8 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -26,18 +26,26 @@ android:layout_marginTop="16dp" android:padding="14dp" android:background="@drawable/k2go_card_bg"> - + + + - + + + + - + + + + @@ -50,7 +58,9 @@ android:layout_marginTop="12dp" android:padding="14dp" android:background="@drawable/k2go_card_bg"> - + + + @@ -64,7 +74,9 @@ android:layout_marginTop="12dp" android:padding="14dp" android:background="@drawable/k2go_card_bg"> - + + + From 68a47bbb15d7ecad2556e0537f488512c8f6fb32 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 10:36:50 -0600 Subject: [PATCH 32/55] =?UTF-8?q?ADFA-4725:=20Step=202=20A/B=20=E2=80=94?= =?UTF-8?q?=20drop=20the=20weighted=20spacer=20(Download=20follows=20conte?= =?UTF-8?q?nt,=20per=20design);=20fixes=20the=20oversized=20card=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/main/res/layout/fragment_k2go_setup_step2a.xml | 1 - .../app/src/main/res/layout/fragment_k2go_setup_step2b.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 92196fc8..a08bdc4d 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -98,7 +98,6 @@ - diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index 6b6c9a67..a1e5b343 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -49,7 +49,6 @@ - From b92c1ad7a2f13f04826425555f3dba7f02db690f Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 11:01:43 -0600 Subject: [PATCH 33/55] =?UTF-8?q?ADFA-4725:=20Step=202=20A/B=20=E2=80=94?= =?UTF-8?q?=20remove=20fillViewport=20(ScrollView=20measured=20natural=20h?= =?UTF-8?q?eight;=20fixes=20Wikipedia=20card=20taking=20the=20whole=20scre?= =?UTF-8?q?en)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/main/res/layout/fragment_k2go_setup_step2a.xml | 2 +- .../app/src/main/res/layout/fragment_k2go_setup_step2b.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index a08bdc4d..68aa9021 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -1,7 +1,7 @@ + android:background="@color/k2go_paper"> diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index a1e5b343..78b1174d 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -1,7 +1,7 @@ + android:background="@color/k2go_paper"> From 47096442941e06e14e649a9eca1748b4668aeddc Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 11:08:31 -0600 Subject: [PATCH 34/55] =?UTF-8?q?ADFA-4725:=20Step=202=20Option=20B=20?= =?UTF-8?q?=E2=80=94=20Include-Wikipedia=20checkbox=20+=20per-step=20Skip?= =?UTF-8?q?=20link=20(Back=20left=20/=20Skip=20right);=20total=20reflects?= =?UTF-8?q?=20skip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redesign/Step2OptionBFragment.java | 30 +++++++++++++------ .../res/layout/fragment_k2go_setup_step2b.xml | 9 ++++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 816c8fd5..1c4dd76e 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -22,9 +23,8 @@ import org.iiab.controller.install.presentation.InstallService; /** - * Step 2 Content — Option B (5-step map + arc Gauge). Guided: one source per step, a - * projected-use gauge (reuses the old UI's MultiResourceGaugeView), Wikipedia coverage/detail - * on step 1, Download on Review. Same install payload as Option A; picks shared via the Activity. + * Step 2 Content — Option B (5-step map + arc Gauge). One source per step, projected-use gauge, + * per-step "Skip". Wikipedia is includable (checkbox / Skip); Maps is fixed; Books/Courses WIP. */ public class Step2OptionBFragment extends Fragment { @@ -35,11 +35,12 @@ public class Step2OptionBFragment extends Fragment { private int step = 0; private String lang; - private boolean everything = false, pictures = true; - private double lastTotal = 0; + private boolean everything = false, pictures = true, wikiIncluded = true; + private double lastTotal = 0, pOs = 1.4, pMaps = 0.2, pKiwix = 4.6; private final TextView[] dots = new TextView[5]; - private TextView caption, legend, btnBack, btnNext, info; + private TextView caption, legend, btnBack, btnNext, btnSkip, info; + private CheckBox wikiCheck; private MultiResourceGaugeView gauge; private TextView covPop, covEvery, detPic, detTxt; private View wikiBlock; @@ -65,12 +66,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c info = v.findViewById(R.id.k2go_info_text); btnBack = v.findViewById(R.id.k2go_btn_back); btnNext = v.findViewById(R.id.k2go_btn_next); + btnSkip = v.findViewById(R.id.k2go_btn_skip); + wikiCheck = v.findViewById(R.id.k2go_wiki_check); wikiBlock = v.findViewById(R.id.k2go_wiki_block); covPop = v.findViewById(R.id.k2go_cov_popular); covEvery = v.findViewById(R.id.k2go_cov_everything); detPic = v.findViewById(R.id.k2go_det_pictures); detTxt = v.findViewById(R.id.k2go_det_text); + wikiCheck.setChecked(wikiIncluded); + wikiCheck.setOnClickListener(x -> { wikiIncluded = wikiCheck.isChecked(); refreshProjection(); }); covPop.setOnClickListener(x -> { everything = false; persist(); refreshProjection(); }); covEvery.setOnClickListener(x -> { everything = true; persist(); refreshProjection(); }); detPic.setOnClickListener(x -> { pictures = true; persist(); refreshProjection(); }); @@ -79,6 +84,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (step > 0) { step--; render(); } else if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); }); + btnSkip.setOnClickListener(x -> { + if (step == 0) { wikiIncluded = false; wikiCheck.setChecked(false); refreshProjection(); } + if (step < 4) { step++; render(); } + }); btnNext.setOnClickListener(x -> { if (step < 4) { step++; render(); } else startDownload(); }); AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); @@ -103,6 +112,7 @@ private void render() { info.setVisibility(step == 0 ? View.GONE : View.VISIBLE); info.setText(INFO[step]); btnBack.setVisibility(step == 0 ? View.INVISIBLE : View.VISIBLE); + btnSkip.setVisibility(step < 4 ? View.VISIBLE : View.GONE); toggleLabels(); updateNextLabel(); } @@ -128,8 +138,10 @@ private void refreshProjection() { @Override public void onCalculated(InstallationPlanner.StorageProjection p) { if (!isAdded()) return; - lastTotal = p.totalSize; - updateGauge(p.osSize, p.mapsSize + p.kiwixSize); + pOs = p.osSize; pMaps = p.mapsSize; pKiwix = p.kiwixSize; + double picks = (wikiIncluded ? pKiwix : 0) + pMaps; + lastTotal = pOs + picks; + updateGauge(pOs, picks); updateNextLabel(); } @Override public void onError(String e) { } @@ -160,7 +172,7 @@ private void startDownload() { i.putExtra(InstallService.EXTRA_COMPANION, true); i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); - i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variantKey()); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, wikiIncluded ? variantKey() : null); i.putExtra(InstallService.EXTRA_REINSTALL, false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index 78b1174d..717d3b26 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -28,6 +28,7 @@ + @@ -52,8 +53,10 @@ - + + + + + From 87359c3613892dc550c1d31f925b40793e554e44 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 11:09:41 -0600 Subject: [PATCH 35/55] =?UTF-8?q?ADFA-4725:=20add=20'Skip=20for=20now'=20?= =?UTF-8?q?=E2=80=94=20wizard=20step=204=20+=20Step=201=20System=20open=20?= =?UTF-8?q?the=20app=20empty=20(no=20install);=20set=20up=20later=20via=20?= =?UTF-8?q?Get=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iiab/controller/redesign/Step1SystemFragment.java | 6 ++++++ .../org/iiab/controller/redesign/WizardActivity.java | 5 +++++ .../app/src/main/res/layout/activity_k2go_wizard.xml | 2 ++ .../src/main/res/layout/fragment_k2go_setup_step1.xml | 9 +++++---- controller/app/src/main/res/values/strings_k2go.xml | 1 + 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java index f38b495f..235a8084 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -79,6 +79,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); root.findViewById(R.id.k2go_step1_back).setOnClickListener(v -> requireActivity().finish()); + root.findViewById(R.id.k2go_step1_skip_now).setOnClickListener(v -> { + requireContext().getSharedPreferences(getString(R.string.pref_file_internal), android.content.Context.MODE_PRIVATE) + .edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); + startActivity(new android.content.Intent(requireContext(), LibraryActivity.class)); + requireActivity().finish(); + }); select(selected); return root; } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java index ba4ea6ac..c449b614 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java @@ -74,6 +74,11 @@ protected void onCreate(Bundle b) { startActivity(new Intent(this, LibraryActivity.class)); finish(); }); + findViewById(R.id.wiz_skip_for_now).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, LibraryActivity.class)); + finish(); + }); primary.setOnClickListener(v -> onPrimary()); back.setOnClickListener(v -> goBack()); diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml index a5b7de1f..91d4cc5c 100644 --- a/controller/app/src/main/res/layout/activity_k2go_wizard.xml +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -84,6 +84,8 @@ android:text="@string/k2go_internet_download_sub" android:textColor="@color/k2go_muted" android:textAppearance="?attr/textAppearanceBodySmall" /> + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml index 04f7e99f..e29f0ad1 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml @@ -90,9 +90,10 @@ android:background="@drawable/k2go_primary_bg" android:clickable="true" android:focusable="true" /> - + + + + + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 1d644aa5..67e5456b 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -77,4 +77,5 @@ Kiwix presets exist; Kolibri can\'t be preset yet — set up on the device after install. added after install Tap a source to Add or Skip it. Skip all = just the system. + Skip for now › From 588426fbbf58e3b4f221d9804c8ffd01323781b3 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 14:53:17 -0600 Subject: [PATCH 36/55] ADFA-4725: Wikipedia install honors the UI pick (no silent "Everything" substitution) Two catalog-resolution fixes in InstallationPlanner: - Family-safe variant resolution. When an explicit variant is requested but absent for that language, degrade only DOWNWARD within the same coverage family (top1m -> top, and across detail), never escalating. A "Popular" pick can no longer be silently replaced by the full "Everything" (all_maxi) ZIM via the old priority list. If nothing in-family exists, no Wikipedia is installed rather than a different corpus. The priority auto-select is kept only for legacy callers that pass no explicit variant. - Newest date by declaration. The listing can expose more than one date for the same variant; keep only the newest ("YYYY-MM" compares chronologically), instead of whichever row happened to come last in the HTML. Also adds CANONICAL_VARIANTS + availableVariants() so the UI can render only the versions a language actually offers. --- .../iiab/controller/InstallationPlanner.java | 88 ++++++++++++++++--- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java index 129d2283..2602d724 100644 --- a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java +++ b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java @@ -80,6 +80,54 @@ public static void wipeCache(Context context) { if (cacheFile.exists()) cacheFile.delete(); } + /** Canonical Wikipedia variants, ordered coverage-desc then detail-desc. */ + public static final String[] CANONICAL_VARIANTS = { + "all_maxi", "all_nopic", "all_mini", + "top1m_maxi", "top1m_nopic", "top1m_mini", + "top_maxi", "top_nopic", "top_mini" + }; + + /** Variants actually present for a language, in canonical order (for the UI to render). */ + public static java.util.List availableVariants(JSONObject langData) { + java.util.List out = new java.util.ArrayList<>(); + if (langData == null) return out; + for (String key : CANONICAL_VARIANTS) if (langData.has(key)) out.add(key); + return out; + } + + private static String coverageOf(String variant) { + if (variant.startsWith("all_")) return "all"; + if (variant.startsWith("top1m_")) return "top1m"; + return "top"; + } + private static String detailOf(String variant) { + if (variant.endsWith("_maxi")) return "maxi"; + if (variant.endsWith("_mini")) return "mini"; + return "nopic"; + } + + /** + * Resolve an explicit variant against what the language actually offers, degrading only + * DOWNWARD within the coverage family (top1m -> top) and across detail, never escalating. + * Returns a present key or null. + */ + private static String resolveInFamily(JSONObject langData, String requested) { + if (langData == null) return null; + if (langData.has(requested)) return requested; + String cov = coverageOf(requested); + String det = detailOf(requested); + String[] covLadder = cov.equals("all") ? new String[]{"all"} + : cov.equals("top1m") ? new String[]{"top1m", "top"} + : new String[]{"top"}; + String[] detLadder = det.equals("maxi") ? new String[]{"maxi", "nopic", "mini"} + : det.equals("nopic") ? new String[]{"nopic", "mini", "maxi"} + : new String[]{"mini", "nopic", "maxi"}; + for (String cc : covLadder) + for (String dd : detLadder) + if (langData.has(cc + "_" + dd)) return cc + "_" + dd; + return null; + } + public static void getOrFetchCatalog(Context context, CacheListener listener) { new Thread(() -> { File cacheFile = new File(context.getCacheDir(), CACHE_FILE_NAME); @@ -134,12 +182,19 @@ public static void getOrFetchCatalog(Context context, CacheListener listener) { if (!database.has(lang)) database.put(lang, new JSONObject()); - // NEW: Store size AND exact filename - JSONObject variantData = new JSONObject(); - variantData.put("size", sizeGb); - variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim"); - - database.getJSONObject(lang).put(variant, variantData); + // Store size, exact filename AND date. The listing can hold more than + // one date for the same variant; keep ONLY the newest by declared + // intent ("YYYY-MM" compares lexicographically = chronologically), + // not by whichever row happens to come last in the HTML. + JSONObject langObj = database.getJSONObject(lang); + JSONObject existing = langObj.optJSONObject(variant); + if (existing == null || date.compareTo(existing.optString("date", "")) > 0) { + JSONObject variantData = new JSONObject(); + variantData.put("size", sizeGb); + variantData.put("date", date); + variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim"); + langObj.put(variant, variantData); + } } } @@ -234,19 +289,26 @@ public void onReady(JSONObject catalog) { } if (langData != null) { - if (overrideVariant != null && langData.has(overrideVariant)) { - JSONObject vData = langData.optJSONObject(overrideVariant); - // VDATA NULL PROTECTION - if (vData != null) { - kiwixSize = vData.optDouble("size", 0.0); - resolvedFile = vData.optString("file", null); + if (overrideVariant != null) { + // Explicit pick from the UI. Honor it exactly, or degrade WITHIN + // the same coverage family (e.g. top1m_maxi -> top_maxi), NEVER + // escalating — a "Popular" pick must not silently become the full + // "Everything" ZIM. If nothing in-family exists, install no + // Wikipedia rather than substituting a different corpus. + String resolved = resolveInFamily(langData, overrideVariant); + if (resolved != null) { + JSONObject vData = langData.optJSONObject(resolved); + if (vData != null) { + kiwixSize = vData.optDouble("size", 0.0); + resolvedFile = vData.optString("file", null); + } } } else { + // Legacy callers with no explicit pick: priority auto-select. String[] priorities = {"all_maxi", "top1m_maxi", "all_nopic", "top1m_nopic", "top_maxi", "all_mini", "top_nopic"}; for (String p : priorities) { if (langData.has(p)) { JSONObject vData = langData.optJSONObject(p); - // VDATA NULL PROTECTION if (vData != null) { double size = vData.optDouble("size", 0.0); if (size <= finalLimit) { From dcfeb96e617e0a27e6b93ed09b4f100b2eaef29a Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 15:18:53 -0600 Subject: [PATCH 37/55] =?UTF-8?q?ADFA-4725:=20Wikipedia=20version=20picker?= =?UTF-8?q?=20=E2=80=94=20List/Grouped,=20multi-select=20(min=20one),=20hi?= =?UTF-8?q?de-unavailable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the fixed 2x2 (Popular/Everything x Pictures/Text) with a shared, honest picker over what a language actually offers. - WikiVersionPicker (reusable): two views over the same data — List (sortable by name or size) and Grouped-by-coverage (Everything / Top 1M / Popular, only the groups a language has). Multi-select checkboxes, min one to continue; only the variants present in the catalog are shown, the rest are hidden. Mounted in both workflows: integrated among the modules in Option A, full-screen (step 1 of 5) in Option B. - WikiVariants: i18n-ready dictionary (coverage/detail names + descriptions as string resources) and helpers, incl. primary() = the single ZIM v1 installs from a multi-selection (first in canonical order). - Selection lives in SetupLibraryActivity (shared Set) so picks and the view mode survive the A/B flip. - v1 downloads one ZIM (the primary); the UI is multi-capable for a later iteration. Skip Wikipedia still installs system-only. Builds on the family-safe resolution from the previous commit: the picker only ever sends a variant the language actually has, so the install can't fall back to a different corpus. --- .../redesign/SetupLibraryActivity.java | 14 +- .../redesign/Step2OptionAFragment.java | 96 ++++---- .../redesign/Step2OptionBFragment.java | 101 +++++--- .../controller/redesign/WikiVariants.java | 71 ++++++ .../redesign/WikiVersionPicker.java | 227 ++++++++++++++++++ .../res/layout/fragment_k2go_setup_step2a.xml | 19 +- .../res/layout/fragment_k2go_setup_step2b.xml | 19 +- .../app/src/main/res/values/strings_k2go.xml | 19 ++ 8 files changed, 445 insertions(+), 121 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java index 6c3f872e..8a0ba8c6 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -15,9 +15,13 @@ public class SetupLibraryActivity extends AppCompatActivity { private InstallationPlanner.Tier selectedTier = InstallationPlanner.Tier.STANDARD; - private boolean contentEverything = false; // false = Popular - private boolean contentPictures = true; // true = With pictures + private boolean contentEverything = false; // legacy (kept for compat; unused by the picker) + private boolean contentPictures = true; // legacy private boolean optionB = false; // A is the default + // Shared Wikipedia selection so picks survive the A/B flip. + private final java.util.LinkedHashSet wikiVariants = new java.util.LinkedHashSet<>(); + private boolean wikiIncluded = true; + private String wikiView = "list"; // "list" | "grouped" @Override protected void onCreate(Bundle savedInstanceState) { @@ -38,6 +42,12 @@ protected void onCreate(Bundle savedInstanceState) { public boolean isPictures() { return contentPictures; } public void setPictures(boolean b) { contentPictures = b; } + public java.util.LinkedHashSet getWikiVariants() { return wikiVariants; } + public boolean isWikiIncluded() { return wikiIncluded; } + public void setWikiIncluded(boolean b) { wikiIncluded = b; } + public String getWikiView() { return wikiView; } + public void setWikiView(String v) { wikiView = v; } + private Fragment step2Fragment() { return optionB ? new Step2OptionBFragment() : new Step2OptionAFragment(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index bc3a255b..3a3f6a82 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -1,7 +1,6 @@ package org.iiab.controller.redesign; import android.content.Intent; -import android.content.res.ColorStateList; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -15,6 +14,7 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import java.util.Locale; +import java.util.Set; import org.iiab.controller.InstallationPlanner; import org.iiab.controller.R; import org.iiab.controller.SystemStateEvaluator; @@ -23,28 +23,30 @@ import org.json.JSONObject; /** - * Step 2 Content — Option A (Expandable + Bar). Collapsible sources: Wikipedia (real Kiwix - * variant, skippable), Books (Coming soon, cosmetic skip), Maps (fixed 0.2 GB), Courses - * (disabled). Bar + total reflect the picks; Download drives InstallService (companion + - * Wikipedia variant). Skip everything -> just the system. + * Step 2 Content — Option A (Expandable + Bar). Wikipedia is one collapsible module among Books + * (Coming soon), Maps (fixed 0.2 GB) and Courses (disabled). Its versions are chosen with the + * shared {@link WikiVersionPicker} (List/Grouped, multi-select, min one). Bar + total reflect the + * picks; Download drives InstallService. v1 installs the primary of the selection. */ public class Step2OptionAFragment extends Fragment { - private static final double BOOKS_GB = 0.0, MAPS_GB = 0.2, WIKI_FALLBACK = 4.6; + private static final double BOOKS_GB = 0.0, MAPS_GB = 0.2; - private boolean everything = false, pictures = true; private boolean wikiInc = true, booksInc = true; private final boolean mapsInc = true; // Maps content is fixed for now private String lang; private JSONObject langData; private double osGb = 1.4; + private Set sel; + private WikiVersionPicker picker; + private CheckBox wikiCheck, booksCheck, mapsCheck; private View wikiBody, booksBody, mapsBody; private TextView wikiChevron, booksChevron, mapsChevron, wikiSkip, booksSkip; private View wikiCard, booksCard; - private TextView wikiSize, booksSize, legend, download; - private TextView covPop, covEvery, detPic, detTxt; + private TextView wikiSize, booksSize, legend, download, wikiHint; + private ViewGroup versions; private LinearLayout bar; private View bU, bS, bP, bF; @@ -52,26 +54,20 @@ private SetupLibraryActivity act() { return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; } private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } - private String popularBase() { return (langData != null && langData.has("top1m_maxi")) ? "top1m" : "top"; } - private String coverageBase() { return everything ? "all" : popularBase(); } - private String variantKey() { return coverageBase() + "_" + (pictures ? "maxi" : "nopic"); } - private double sizeOf(String variant) { - if (langData == null) return -1; - JSONObject o = langData.optJSONObject(variant); - return o == null ? -1 : o.optDouble("size", -1); - } + private double wikiSizeGb() { - double s = sizeOf(variantKey()); - return s >= 0 ? s : WIKI_FALLBACK; + String p = WikiVariants.primary(sel); + double s = (p == null) ? -1 : WikiVariants.sizeGb(langData, p); + return s >= 0 ? s : 0; } - private static String gb(double s) { return s >= 0 ? String.format(Locale.US, "%.1fG", s) : "—"; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { View v = inflater.inflate(R.layout.fragment_k2go_setup_step2a, c, false); lang = ContentLanguage.systemDefault(); - if (act() != null) { everything = act().isEverything(); pictures = act().isPictures(); } + sel = (act() != null) ? act().getWikiVariants() : new java.util.LinkedHashSet<>(); + if (act() != null) wikiInc = act().isWikiIncluded(); bar = v.findViewById(R.id.k2go_bar); bU = v.findViewById(R.id.k2go_bar_used); bS = v.findViewById(R.id.k2go_bar_system); @@ -84,10 +80,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c wikiSkip = v.findViewById(R.id.k2go_wiki_skip); booksSkip = v.findViewById(R.id.k2go_books_skip); wikiCard = v.findViewById(R.id.k2go_wiki_card); booksCard = v.findViewById(R.id.k2go_books_card); wikiSize = v.findViewById(R.id.k2go_wiki_size); booksSize = v.findViewById(R.id.k2go_books_size); - covPop = v.findViewById(R.id.k2go_cov_popular); covEvery = v.findViewById(R.id.k2go_cov_everything); - detPic = v.findViewById(R.id.k2go_det_pictures); detTxt = v.findViewById(R.id.k2go_det_text); + versions = v.findViewById(R.id.k2go_wiki_versions); + wikiHint = v.findViewById(R.id.k2go_wiki_hint); colorSeg(bU, R.color.k2go_muted); colorSeg(bS, R.color.k2go_teal); colorSeg(bP, R.color.k2go_leaf); colorSeg(bF, R.color.k2go_hairline); + picker = new WikiVersionPicker(requireContext(), versions, sel, + act() != null ? act().getWikiView() : "list", this::refresh); + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); v.findViewById(R.id.k2go_wiki_header).setOnClickListener(x -> toggle(wikiBody, wikiChevron)); @@ -100,17 +99,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c booksSkip.setOnClickListener(x -> setBooks(!booksInc)); mapsCheck.setOnClickListener(x -> mapsCheck.setChecked(true)); // fixed - covPop.setOnClickListener(x -> { everything = false; persist(); refresh(); }); - covEvery.setOnClickListener(x -> { if (covEvery.isEnabled()) { everything = true; persist(); refresh(); } }); - detPic.setOnClickListener(x -> { pictures = true; persist(); refresh(); }); - detTxt.setOnClickListener(x -> { pictures = false; persist(); refresh(); }); - download.setOnClickListener(x -> startDownload()); v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> { if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); }); - // system size (no network) and Kiwix catalog (cached) both drive the numbers. InstallationPlanner.calculateProjectedSize(requireContext(), tier(), false, lang, null, new InstallationPlanner.PlanResultListener() { @Override public void onCalculated(InstallationPlanner.StorageProjection p) { if (isAdded()) { osGb = p.osSize; refresh(); } } @@ -121,17 +114,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (!isAdded()) return; langData = catalog.optJSONObject(lang); if (langData == null) langData = catalog.optJSONObject("en"); + picker.setLangData(langData); + if (wikiInc) picker.selectDefaultIfEmpty(); + picker.render(); refresh(); } @Override public void onError(String e) { } }); - refresh(); + setWiki(wikiInc); return v; } - private void persist() { if (act() != null) { act().setEverything(everything); act().setPictures(pictures); } } - private void toggle(View body, TextView chevron) { boolean show = body.getVisibility() != View.VISIBLE; body.setVisibility(show ? View.VISIBLE : View.GONE); @@ -143,7 +137,9 @@ private void setWiki(boolean inc) { wikiCheck.setChecked(inc); wikiCard.setAlpha(inc ? 1f : 0.55f); wikiSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); - if (!inc) { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } + if (inc) { wikiBody.setVisibility(View.VISIBLE); wikiChevron.setText("▾"); } + else { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } + if (act() != null) act().setWikiIncluded(inc); refresh(); } private void setBooks(boolean inc) { @@ -155,29 +151,21 @@ private void setBooks(boolean inc) { refresh(); } + private boolean wikiInvalid() { return wikiInc && sel.isEmpty(); } + private void refresh() { - double free = StorageInfo.freeGb(); - // Wikipedia variant labels + "Everything won't fit" guard - boolean everyFits = sizeOf("all_" + (pictures ? "maxi" : "nopic")) < 0 - || sizeOf("all_" + (pictures ? "maxi" : "nopic")) <= (free - osGb); - label(covPop, "Popular", sizeOf(popularBase() + "_" + (pictures ? "maxi" : "nopic")), !everything, true); - label(covEvery, "Everything", sizeOf("all_" + (pictures ? "maxi" : "nopic")), everything, everyFits); - label(detPic, "With pictures", sizeOf(coverageBase() + "_maxi"), pictures, true); - label(detTxt, "Text only", sizeOf(coverageBase() + "_nopic"), !pictures, true); - wikiSize.setText(wikiInc ? gb(wikiSizeGb()) : getString(R.string.k2go_skipped)); + if (act() != null) { act().setWikiView(picker.getMode()); act().setWikiIncluded(wikiInc); } + wikiHint.setVisibility(wikiInvalid() ? View.VISIBLE : View.GONE); + wikiSize.setText(!wikiInc ? getString(R.string.k2go_skipped) + : sel.isEmpty() ? "—" : WikiVariants.gb(wikiSizeGb())); booksSize.setText("0 GB"); - double picks = (wikiInc ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0) + (mapsInc ? MAPS_GB : 0); + double picks = (wikiInc && !sel.isEmpty() ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0) + (mapsInc ? MAPS_GB : 0); applyBar(osGb, picks); download.setText(String.format(Locale.US, "Download library · %.1f GB", osGb + picks)); - } - - private void label(TextView t, String name, double size, boolean on, boolean enabled) { - t.setText(name + " " + gb(size)); - t.setEnabled(enabled); - t.setAlpha(enabled ? 1f : 0.4f); - t.setBackgroundResource(on && enabled ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); - t.setTextColor(ContextCompat.getColor(requireContext(), on && enabled ? R.color.k2go_on_teal : R.color.k2go_ink)); + boolean ok = !wikiInvalid(); + download.setEnabled(ok); + download.setAlpha(ok ? 1f : 0.5f); } private void applyBar(double systemGb, double picksGb) { @@ -191,14 +179,16 @@ private void applyBar(double systemGb, double picksGb) { } private void startDownload() { - boolean companion = wikiInc || booksInc || mapsInc; + if (wikiInvalid()) { Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); return; } + String variant = wikiInc ? WikiVariants.primary(sel) : null; + boolean companion = variant != null || booksInc || mapsInc; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); i.putExtra(InstallService.EXTRA_TIER, tier().name()); i.putExtra(InstallService.EXTRA_COMPANION, companion); i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); - i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, wikiInc ? variantKey() : null); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); i.putExtra(InstallService.EXTRA_REINSTALL, false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 1c4dd76e..cb203e73 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -15,16 +15,19 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Set; import org.iiab.controller.InstallationPlanner; import org.iiab.controller.MultiResourceGaugeView; import org.iiab.controller.R; import org.iiab.controller.SystemStateEvaluator; import org.iiab.controller.applang.data.ContentLanguage; import org.iiab.controller.install.presentation.InstallService; +import org.json.JSONObject; /** - * Step 2 Content — Option B (5-step map + arc Gauge). One source per step, projected-use gauge, - * per-step "Skip". Wikipedia is includable (checkbox / Skip); Maps is fixed; Books/Courses WIP. + * Step 2 Content — Option B (5-step map + arc Gauge). Wikipedia has a full screen of its own + * (step 1 of 5) hosting the shared {@link WikiVersionPicker} (List/Grouped, multi-select, min + * one). Maps is fixed; Books/Courses WIP. v1 installs the primary of the selection. */ public class Step2OptionBFragment extends Fragment { @@ -35,28 +38,32 @@ public class Step2OptionBFragment extends Fragment { private int step = 0; private String lang; - private boolean everything = false, pictures = true, wikiIncluded = true; + private JSONObject langData; + private boolean wikiIncluded = true; private double lastTotal = 0, pOs = 1.4, pMaps = 0.2, pKiwix = 4.6; + private Set sel; + private WikiVersionPicker picker; + private final TextView[] dots = new TextView[5]; - private TextView caption, legend, btnBack, btnNext, btnSkip, info; + private TextView caption, legend, btnBack, btnNext, btnSkip, info, wikiHint; private CheckBox wikiCheck; private MultiResourceGaugeView gauge; - private TextView covPop, covEvery, detPic, detTxt; private View wikiBlock; + private ViewGroup versions; private SetupLibraryActivity act() { return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; } private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } - private String variantKey() { return (everything ? "all" : "top1m") + "_" + (pictures ? "maxi" : "nopic"); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { View v = inflater.inflate(R.layout.fragment_k2go_setup_step2b, c, false); lang = ContentLanguage.systemDefault(); - if (act() != null) { everything = act().isEverything(); pictures = act().isPictures(); } + sel = (act() != null) ? act().getWikiVariants() : new java.util.LinkedHashSet<>(); + if (act() != null) wikiIncluded = act().isWikiIncluded(); int[] ids = {R.id.k2go_sd0, R.id.k2go_sd1, R.id.k2go_sd2, R.id.k2go_sd3, R.id.k2go_sd4}; for (int i = 0; i < 5; i++) dots[i] = v.findViewById(ids[i]); @@ -69,36 +76,66 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c btnSkip = v.findViewById(R.id.k2go_btn_skip); wikiCheck = v.findViewById(R.id.k2go_wiki_check); wikiBlock = v.findViewById(R.id.k2go_wiki_block); - covPop = v.findViewById(R.id.k2go_cov_popular); - covEvery = v.findViewById(R.id.k2go_cov_everything); - detPic = v.findViewById(R.id.k2go_det_pictures); - detTxt = v.findViewById(R.id.k2go_det_text); + versions = v.findViewById(R.id.k2go_wiki_versions); + wikiHint = v.findViewById(R.id.k2go_wiki_hint); + + picker = new WikiVersionPicker(requireContext(), versions, sel, + act() != null ? act().getWikiView() : "list", () -> { updateHint(); refreshProjection(); }); wikiCheck.setChecked(wikiIncluded); - wikiCheck.setOnClickListener(x -> { wikiIncluded = wikiCheck.isChecked(); refreshProjection(); }); - covPop.setOnClickListener(x -> { everything = false; persist(); refreshProjection(); }); - covEvery.setOnClickListener(x -> { everything = true; persist(); refreshProjection(); }); - detPic.setOnClickListener(x -> { pictures = true; persist(); refreshProjection(); }); - detTxt.setOnClickListener(x -> { pictures = false; persist(); refreshProjection(); }); + wikiCheck.setOnClickListener(x -> { + wikiIncluded = wikiCheck.isChecked(); + persist(); + versions.setVisibility(wikiIncluded ? View.VISIBLE : View.GONE); + updateHint(); + refreshProjection(); + }); btnBack.setOnClickListener(x -> { if (step > 0) { step--; render(); } else if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); }); btnSkip.setOnClickListener(x -> { - if (step == 0) { wikiIncluded = false; wikiCheck.setChecked(false); refreshProjection(); } + if (step == 0) { + wikiIncluded = false; + wikiCheck.setChecked(false); + versions.setVisibility(View.GONE); + persist(); + updateHint(); + refreshProjection(); + } if (step < 4) { step++; render(); } }); - btnNext.setOnClickListener(x -> { if (step < 4) { step++; render(); } else startDownload(); }); + btnNext.setOnClickListener(x -> { + if (step == 0 && wikiIncluded && sel.isEmpty()) { + updateHint(); + Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); + return; + } + if (step < 4) { step++; render(); } else startDownload(); + }); AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); + InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { + @Override public void onReady(JSONObject catalog) { + if (!isAdded()) return; + langData = catalog.optJSONObject(lang); + if (langData == null) langData = catalog.optJSONObject("en"); + picker.setLangData(langData); + if (wikiIncluded) picker.selectDefaultIfEmpty(); + picker.render(); + updateHint(); + refreshProjection(); + } + @Override public void onError(String e) { } + }); + render(); refreshProjection(); return v; } private void persist() { - if (act() != null) { act().setEverything(everything); act().setPictures(pictures); } - toggleLabels(); + if (act() != null) { act().setWikiIncluded(wikiIncluded); act().setWikiView(picker.getMode()); } } private void render() { @@ -109,37 +146,32 @@ private void render() { } caption.setText(NAMES[step]); wikiBlock.setVisibility(step == 0 ? View.VISIBLE : View.GONE); + versions.setVisibility(step == 0 && wikiIncluded ? View.VISIBLE : View.GONE); info.setVisibility(step == 0 ? View.GONE : View.VISIBLE); info.setText(INFO[step]); btnBack.setVisibility(step == 0 ? View.INVISIBLE : View.VISIBLE); btnSkip.setVisibility(step < 4 ? View.VISIBLE : View.GONE); - toggleLabels(); + updateHint(); updateNextLabel(); } - private void toggleLabels() { - hi(covPop, !everything); hi(covEvery, everything); - hi(detPic, pictures); hi(detTxt, !pictures); - } - private void hi(TextView t, boolean on) { - t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); - t.setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_ink)); + private void updateHint() { + wikiHint.setVisibility(step == 0 && wikiIncluded && sel.isEmpty() ? View.VISIBLE : View.GONE); } private void updateNextLabel() { - btnNext.setText(step == 4 - ? String.format(Locale.US, "Download library · %.1f GB", lastTotal) - : "Next"); + btnNext.setText(step == 4 ? String.format(Locale.US, "Download library · %.1f GB", lastTotal) : "Next"); } private void refreshProjection() { - InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variantKey(), + String variant = wikiIncluded ? WikiVariants.primary(sel) : null; + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variant, new InstallationPlanner.PlanResultListener() { @Override public void onCalculated(InstallationPlanner.StorageProjection p) { if (!isAdded()) return; pOs = p.osSize; pMaps = p.mapsSize; pKiwix = p.kiwixSize; - double picks = (wikiIncluded ? pKiwix : 0) + pMaps; + double picks = ((wikiIncluded && variant != null) ? pKiwix : 0) + pMaps; lastTotal = pOs + picks; updateGauge(pOs, picks); updateNextLabel(); @@ -166,13 +198,14 @@ private void updateGauge(double systemGb, double picksGb) { } private void startDownload() { + String variant = wikiIncluded ? WikiVariants.primary(sel) : null; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); i.putExtra(InstallService.EXTRA_TIER, tier().name()); i.putExtra(InstallService.EXTRA_COMPANION, true); i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); - i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, wikiIncluded ? variantKey() : null); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); i.putExtra(InstallService.EXTRA_REINSTALL, false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java new file mode 100644 index 00000000..340ee4ef --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java @@ -0,0 +1,71 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import java.util.Locale; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.json.JSONObject; + +/** + * Colloquial, i18n-ready labels for the Kiwix Wikipedia variants (the "dictionary"). Coverage is + * data-driven (all / top1m / top) — never a fixed dichotomy. All labels come from string + * resources so a later l10n pass can translate them without touching this code. + */ +public final class WikiVariants { + private WikiVariants() {} + + public static String coverageOf(String key) { + if (key.startsWith("all_")) return "all"; + if (key.startsWith("top1m_")) return "top1m"; + return "top"; + } + public static String detailOf(String key) { + if (key.endsWith("_maxi")) return "maxi"; + if (key.endsWith("_mini")) return "mini"; + return "nopic"; + } + + public static String coverageName(Context c, String cov) { + switch (cov) { + case "all": return c.getString(R.string.k2go_wiki_cov_all); + case "top1m": return c.getString(R.string.k2go_wiki_cov_top1m); + default: return c.getString(R.string.k2go_wiki_cov_top); + } + } + public static String coverageDesc(Context c, String cov) { + switch (cov) { + case "all": return c.getString(R.string.k2go_wiki_cov_all_desc); + case "top1m": return c.getString(R.string.k2go_wiki_cov_top1m_desc); + default: return c.getString(R.string.k2go_wiki_cov_top_desc); + } + } + public static String detailName(Context c, String det) { + switch (det) { + case "maxi": return c.getString(R.string.k2go_wiki_det_maxi); + case "mini": return c.getString(R.string.k2go_wiki_det_mini); + default: return c.getString(R.string.k2go_wiki_det_nopic); + } + } + public static String label(Context c, String key) { + return coverageName(c, coverageOf(key)) + " · " + detailName(c, detailOf(key)); + } + + public static double sizeGb(JSONObject langData, String key) { + if (langData == null) return -1; + JSONObject o = langData.optJSONObject(key); + return o == null ? -1 : o.optDouble("size", -1); + } + + /** The single ZIM v1 installs from a multi-selection: first in canonical order. */ + public static String primary(Set selected) { + for (String k : InstallationPlanner.CANONICAL_VARIANTS) if (selected.contains(k)) return k; + return null; + } + + public static String gb(double s) { + if (s < 0) return "—"; + if (s >= 1) return String.format(Locale.US, "%.1f GB", s); + return Math.round(s * 1000) + " MB"; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java new file mode 100644 index 00000000..2367fbbb --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java @@ -0,0 +1,227 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.core.content.ContextCompat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.json.JSONObject; + +/** + * Reusable Wikipedia-version picker. Renders a List (sortable by name/size) OR a Grouped-by- + * coverage view of ONLY the variants a language actually offers, multi-select, into a + * caller-provided container. The selection Set is shared and mutated in place so picks survive + * the A/B flip. v1 downloads a single ZIM (WikiVariants.primary of the selection); the UI is + * multi-capable for a later iteration. + */ +public class WikiVersionPicker { + public interface OnChange { void changed(); } + + private final Context ctx; + private final ViewGroup container; + private final Set selected; + private final OnChange onChange; + private JSONObject langData; + + private String mode; // "list" | "grouped" + private String sortKey = "size"; // "size" | "name" + private int sortDir = 1; // 1 asc, -1 desc + + public WikiVersionPicker(Context ctx, ViewGroup container, Set selected, String mode, OnChange onChange) { + this.ctx = ctx; + this.container = container; + this.selected = selected; + this.mode = (mode == null) ? "list" : mode; + this.onChange = onChange; + } + + public void setLangData(JSONObject ld) { this.langData = ld; } + public String getMode() { return mode; } + public void setMode(String m) { this.mode = m; render(); } + + /** Pre-select the smallest available variant so the min-one rule is satisfied by default. */ + public void selectDefaultIfEmpty() { + if (!selected.isEmpty() || langData == null) return; + String best = null; + double bs = Double.MAX_VALUE; + for (String k : InstallationPlanner.availableVariants(langData)) { + double s = WikiVariants.sizeGb(langData, k); + if (s >= 0 && s < bs) { bs = s; best = k; } + } + if (best != null) selected.add(best); + } + + private int dp(int d) { return Math.round(d * ctx.getResources().getDisplayMetrics().density); } + + public void render() { + container.removeAllViews(); + if (container instanceof LinearLayout) ((LinearLayout) container).setOrientation(LinearLayout.VERTICAL); + + container.addView(toggleRow()); + + List a = InstallationPlanner.availableVariants(langData); + if (a.isEmpty()) { container.addView(muted(ctx.getString(R.string.k2go_wiki_none))); return; } + + if ("grouped".equals(mode)) renderGrouped(a); else renderList(a); + } + + private View toggleRow() { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lp.bottomMargin = dp(10); + row.setLayoutParams(lp); + row.addView(chip(ctx.getString(R.string.k2go_wiki_view_list), "list".equals(mode), new View.OnClickListener() { + @Override public void onClick(View v) { setMode("list"); } + })); + View gap = new View(ctx); + gap.setLayoutParams(new LinearLayout.LayoutParams(dp(8), 1)); + row.addView(gap); + row.addView(chip(ctx.getString(R.string.k2go_wiki_view_grouped), "grouped".equals(mode), new View.OnClickListener() { + @Override public void onClick(View v) { setMode("grouped"); } + })); + return row; + } + + private TextView chip(String text, boolean on, View.OnClickListener cl) { + TextView t = new TextView(ctx); + t.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + t.setText(text); + t.setGravity(Gravity.CENTER); + t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + t.setPadding(dp(10), dp(8), dp(10), dp(8)); + t.setTextColor(ContextCompat.getColor(ctx, on ? R.color.k2go_on_teal : R.color.k2go_ink)); + t.setClickable(true); + t.setFocusable(true); + t.setOnClickListener(cl); + return t; + } + + private void renderList(List a) { + LinearLayout head = new LinearLayout(ctx); + head.setOrientation(LinearLayout.HORIZONTAL); + head.setPadding(dp(10), 0, dp(10), dp(8)); + TextView hName = header(ctx.getString(R.string.k2go_wiki_col_version), "name"); + hName.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + head.addView(hName); + head.addView(header(ctx.getString(R.string.k2go_wiki_col_size), "size")); + container.addView(head); + + List list = new ArrayList<>(a); + Collections.sort(list, (x, y) -> { + int c = "size".equals(sortKey) + ? Double.compare(WikiVariants.sizeGb(langData, x), WikiVariants.sizeGb(langData, y)) + : WikiVariants.label(ctx, x).compareToIgnoreCase(WikiVariants.label(ctx, y)); + return c * sortDir; + }); + for (String k : list) container.addView(variantRow(k, WikiVariants.label(ctx, k), 0)); + } + + private TextView header(String text, String key) { + TextView t = new TextView(ctx); + String arrow = sortKey.equals(key) ? (sortDir > 0 ? " ▲" : " ▼") : ""; + t.setText(text + arrow); + t.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + t.setTextSize(12); + t.setClickable(true); + t.setFocusable(true); + t.setOnClickListener(v -> { + if (sortKey.equals(key)) sortDir = -sortDir; else { sortKey = key; sortDir = 1; } + render(); + }); + return t; + } + + private void renderGrouped(List a) { + String[] covs = {"all", "top1m", "top"}; + String[] dets = {"maxi", "nopic", "mini"}; + for (String cov : covs) { + boolean any = false; + for (String det : dets) if (a.contains(cov + "_" + det)) { any = true; break; } + if (!any) continue; + + LinearLayout hd = new LinearLayout(ctx); + hd.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams hlp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + hlp.topMargin = dp(10); + hlp.bottomMargin = dp(6); + hd.setLayoutParams(hlp); + TextView title = new TextView(ctx); + title.setText(WikiVariants.coverageName(ctx, cov)); + title.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + title.setTextSize(14); + TextView desc = new TextView(ctx); + desc.setText(WikiVariants.coverageDesc(ctx, cov)); + desc.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + desc.setTextSize(12); + hd.addView(title); + hd.addView(desc); + container.addView(hd); + + for (String det : dets) { + String k = cov + "_" + det; + if (a.contains(k)) container.addView(variantRow(k, WikiVariants.detailName(ctx, det), dp(10))); + } + } + } + + private View variantRow(final String key, String labelText, int indentPx) { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lp.leftMargin = indentPx; + lp.bottomMargin = dp(6); + row.setLayoutParams(lp); + row.setBackgroundResource(R.drawable.k2go_card_bg); + row.setPadding(dp(10), dp(8), dp(10), dp(8)); + row.setClickable(true); + row.setFocusable(true); + + final CheckBox cb = new CheckBox(ctx); + cb.setChecked(selected.contains(key)); + cb.setClickable(false); + cb.setFocusable(false); + row.addView(cb); + + TextView name = new TextView(ctx); + LinearLayout.LayoutParams nlp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); + nlp.leftMargin = dp(8); + name.setLayoutParams(nlp); + name.setText(labelText); + name.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + name.setTextSize(14); + row.addView(name); + + TextView size = new TextView(ctx); + size.setText(WikiVariants.gb(WikiVariants.sizeGb(langData, key))); + size.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + size.setTextSize(13); + row.addView(size); + + row.setOnClickListener(v -> { + if (selected.contains(key)) selected.remove(key); else selected.add(key); + cb.setChecked(selected.contains(key)); + if (onChange != null) onChange.changed(); + }); + return row; + } + + private TextView muted(String text) { + TextView t = new TextView(ctx); + t.setText(text); + t.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + t.setTextSize(13); + t.setPadding(dp(2), dp(6), dp(2), dp(6)); + return t; + } +} diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml index 68aa9021..93cf3aa1 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -34,22 +34,9 @@ - - - - - - - - - - - - - - - - + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml index 717d3b26..abc28d43 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -29,22 +29,9 @@ - - - - - - - - - - + + added after install Tap a source to Add or Skip it. Skip all = just the system. Skip for now › + + + List + Grouped + Version + Size + Choose versions + Skip Wikipedia + Pick at least one version to continue. + No versions available for this language. + Everything + Every article + Top 1M + ~1M most-read articles + Popular + Most consulted articles + With pictures + Text only + Summaries From 5102c05cb9da3b6991acf56e95c2f553833826f0 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 15:29:23 -0600 Subject: [PATCH 38/55] ADFA-4725: Step 1 edition sizes are real, not hardcoded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The storage bar already used the live rootfs size, but the three edition labels (~1.2 / 1.4 / 2.7 GB) were hardcoded literals in the fragment. They now resolve through the same path as the bar: the live .meta4 size when online, and the last-known RootfsCatalog value (shown instantly, no flicker) when offline — never a number hardcoded in the UI. Adds InstallationPlanner.fallbackOsSizeGb(Tier): a no-network accessor to the RootfsCatalog fallback, used for the instant placeholder before the live value arrives. --- .../iiab/controller/InstallationPlanner.java | 6 +++ .../redesign/Step1SystemFragment.java | 40 +++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java index 2602d724..7f1b1381 100644 --- a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java +++ b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java @@ -342,6 +342,12 @@ public void onError(String error) { *

Performs a (cached) network call; must run off the main thread — it does, * because every caller is inside {@link #calculateProjectedSize}'s worker thread. */ + /** No-network last-known OS size (RootfsCatalog fallback) — instant, for placeholders. */ + public static double fallbackOsSizeGb(Tier tier) { + RootfsCatalog catalog = new RootfsCatalog(); + return ByteFormatter.toGiB(catalog.fallbackBytes(toDomainTier(tier), catalog.detectAbi())); + } + private static double resolveOsSizeGb(Tier tier) { RootfsCatalog catalog = new RootfsCatalog(); RootfsRepository repository = diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java index 235a8084..aa22d9b7 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -1,6 +1,5 @@ package org.iiab.controller.redesign; -import android.content.res.ColorStateList; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -19,14 +18,17 @@ import org.iiab.controller.R; import org.iiab.controller.applang.data.ContentLanguage; -/** Step 1 of "Set up your library": choose the system edition (apps) with a live storage bar. */ +/** Step 1 of "Set up your library": choose the system edition (apps) with a live storage bar. + * Edition sizes are resolved from the rootfs .meta4 (live when online); when offline they show + * the last-known value from RootfsCatalog — never hardcoded in the UI. */ public class Step1SystemFragment extends Fragment { private static final class Edition { - final InstallationPlanner.Tier tier; final String name; final double sizeGb; - final String desc; final boolean recommended; ImageView radio; - Edition(InstallationPlanner.Tier t, String n, double s, String d, boolean r) { - tier = t; name = n; sizeGb = s; desc = d; recommended = r; + final InstallationPlanner.Tier tier; final String name; + final String desc; final boolean recommended; + ImageView radio; TextView sizeView; + Edition(InstallationPlanner.Tier t, String n, String d, boolean r) { + tier = t; name = n; desc = d; recommended = r; } } @@ -50,11 +52,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c tint(barFree, R.color.k2go_hairline); editions.clear(); - editions.add(new Edition(InstallationPlanner.Tier.BASIC, "Basic", 1.2, + editions.add(new Edition(InstallationPlanner.Tier.BASIC, "Basic", "Wikipedia reader app + Books app", false)); - editions.add(new Edition(InstallationPlanner.Tier.STANDARD, "Standard", 1.4, + editions.add(new Edition(InstallationPlanner.Tier.STANDARD, "Standard", "Basic plus Courses app", true)); - editions.add(new Edition(InstallationPlanner.Tier.FULL, "Full", 2.7, + editions.add(new Edition(InstallationPlanner.Tier.FULL, "Full", "Standard plus Maps app (all apps)", false)); LinearLayout host = root.findViewById(R.id.k2go_editions); @@ -62,13 +64,14 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c View row = inflater.inflate(R.layout.view_k2go_edition, host, false); ((TextView) row.findViewById(R.id.k2go_edition_name)).setText(e.name); ((TextView) row.findViewById(R.id.k2go_edition_desc)).setText(e.desc); - ((TextView) row.findViewById(R.id.k2go_edition_size)) - .setText(String.format(Locale.US, "~ %.1f GB", e.sizeGb)); + e.sizeView = row.findViewById(R.id.k2go_edition_size); + e.sizeView.setText(sizeText(InstallationPlanner.fallbackOsSizeGb(e.tier))); // instant last-known row.findViewById(R.id.k2go_edition_reco) .setVisibility(e.recommended ? View.VISIBLE : View.GONE); e.radio = row.findViewById(R.id.k2go_edition_radio); row.setOnClickListener(v -> select(e.tier)); host.addView(row); + resolveEditionSize(e); // refine to the live .meta4 size when online } root.findViewById(R.id.k2go_step1_next).setOnClickListener(v -> { @@ -89,6 +92,20 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return root; } + private static String sizeText(double gb) { return String.format(Locale.US, "~ %.1f GB", gb); } + + private void resolveEditionSize(Edition e) { + InstallationPlanner.calculateProjectedSize(requireContext(), e.tier, false, + ContentLanguage.systemDefault(), null, + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection p) { + if (isAdded() && e.sizeView != null) e.sizeView.setText(sizeText(p.osSize)); + } + @Override public void onError(String error) { /* keep last-known */ } + }); + } + private void select(InstallationPlanner.Tier tier) { selected = tier; if (getActivity() instanceof SetupLibraryActivity) { @@ -103,7 +120,6 @@ private void select(InstallationPlanner.Tier tier) { } private void recomputeBar(InstallationPlanner.Tier tier) { - // System size (no content) resolves without a network read. InstallationPlanner.calculateProjectedSize(requireContext(), tier, false, ContentLanguage.systemDefault(), null, new InstallationPlanner.PlanResultListener() { From 744609a5d8b619f6f695708d06e1dede5188a28e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 16:28:43 -0600 Subject: [PATCH 39/55] =?UTF-8?q?ADFA-4725:=20remove=20dead-end=20feel=20?= =?UTF-8?q?=E2=80=94=20persistent=20Portal=20exit,=20wired/preview=20Setti?= =?UTF-8?q?ngs,=20Connect/Clone=20preview=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the new UI safe for a tester to explore without hitting controls that look functional but do nothing. - Portal: the bottom bar (with Exit) is now persistent instead of starting hidden and auto-hiding after 5s behind a tiny handle. A non-technical user always has an obvious way out; manual hide via the down-chevron still works. - Settings: Language, Theme and About are wired to real actions (system language settings, a night-mode chooser, and an About dialog with the real version). Every not-yet-implemented row is now a visibly dimmed "Soon" preview rather than a normal-looking row that only shows a "Coming soon" toast. - Connect / Clone: a "Preview — not functional yet" banner frames each shell, so the mocked call-to-action bars no longer read as working buttons. Does not change the permissions-step gate (tracked separately). --- .../org/iiab/controller/PortalActivity.java | 13 ++- .../controller/redesign/SettingsFragment.java | 110 +++++++++++++++--- .../main/res/layout/fragment_k2go_clone.xml | 3 + .../main/res/layout/fragment_k2go_connect.xml | 3 + .../app/src/main/res/values/strings_k2go.xml | 1 + 5 files changed, 107 insertions(+), 23 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java index 12ca4cd2..6f513453 100644 --- a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java @@ -99,11 +99,14 @@ protected void onCreate(Bundle savedInstanceState) { Button btnExit = findViewById(R.id.btnExit); Button btnForward = findViewById(R.id.btnForward); - // --- PREPARE HIDDEN BAR --- + // --- PERSISTENT BAR --- + // Keep the nav bar (with Exit) visible so a non-technical user always has an + // obvious way out. The handle is only used after a manual hide (btnHideNav). bottomNav.post(() -> { - bottomNav.setTranslationY(bottomNav.getHeight()); // Move outside the screen + bottomNav.setTranslationY(0); bottomNav.setVisibility(View.VISIBLE); }); + btnHandle.setVisibility(View.GONE); // --- AUTO-HIDE TIMER --- Handler hideHandler = new Handler(Looper.getMainLooper()); @@ -114,10 +117,8 @@ protected void onCreate(Bundle savedInstanceState) { btnHandle.animate().alpha(1f).setDuration(150); }; - Runnable resetTimer = () -> { - hideHandler.removeCallbacks(hideRunnable); - hideHandler.postDelayed(hideRunnable, 5000); - }; + // Persistent bar: never auto-hide. Cancel any pending hide; only btnHideNav hides. + Runnable resetTimer = () -> hideHandler.removeCallbacks(hideRunnable); // --- HANDLE LOGIC (Show Bar) --- btnHandle.setOnClickListener(v -> { diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index d1f6ccb3..ffe6bff1 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -1,6 +1,12 @@ package org.iiab.controller.redesign; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -11,11 +17,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import org.iiab.controller.R; -/** Settings tab (ADFA-4725): Basics (About live) + Advanced (visual) + Turn off K2Go. */ +/** Settings tab (ADFA-4725): Basics (Language/Theme/About wired; rest marked as preview) + + * Advanced (preview) + Turn off K2Go. Preview rows are visibly non-functional so nothing + * looks like a working button that does nothing. */ public class SettingsFragment extends Fragment { private LinearLayout list; @@ -26,28 +35,80 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c View root = inflater.inflate(R.layout.fragment_k2go_settings, c, false); list = root.findViewById(R.id.k2go_settings_list); - View.OnClickListener soon = v -> - Toast.makeText(requireContext(), "Coming soon", Toast.LENGTH_SHORT).show(); - header("BASICS"); - row("Language", "", soon); - row("Permissions", "", soon); - row("Theme", "Light / Follow system / Dark", soon); - row("Share usage statistics", "On", soon); - row("About", versionName(), soon); - row("Send feedback", "", soon); + row("Language", "", v -> openLanguageSettings()); + preview("Permissions", ""); + row("Theme", themeLabel(), v -> chooseTheme()); + preview("Share usage statistics", "On"); + row("About", versionName(), v -> showAbout()); + preview("Send feedback", ""); header("ADVANCED · for power users"); - row("System & modules", "", soon); - row("Backups & recovery", "", soon); - row("Developer tools (ADB)", "", soon); - row("Network & DNS", "", soon); - row("Terminal (Debian)", "", soon); + preview("System & modules", ""); + preview("Backups & recovery", ""); + preview("Developer tools (ADB)", ""); + preview("Network & DNS", ""); + preview("Terminal (Debian)", ""); turnOffRow(); return root; } + // ---- wired actions ---- + + private void openLanguageSettings() { + try { + if (Build.VERSION.SDK_INT >= 33) { + startActivity(new Intent(Settings.ACTION_APP_LOCALE_SETTINGS, + Uri.parse("package:" + requireContext().getPackageName()))); + return; + } + } catch (Exception ignore) { /* fall through */ } + try { + startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:" + requireContext().getPackageName()))); + } catch (Exception e) { + Toast.makeText(requireContext(), "Language settings unavailable on this device", Toast.LENGTH_SHORT).show(); + } + } + + private void chooseTheme() { + final String[] labels = {"Light", "Follow system", "Dark"}; + final int[] modes = { + AppCompatDelegate.MODE_NIGHT_NO, + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + AppCompatDelegate.MODE_NIGHT_YES}; + new AlertDialog.Builder(requireContext()) + .setTitle("Theme") + .setItems(labels, (d, w) -> { + prefs().edit().putInt("k2go_theme", modes[w]).apply(); + AppCompatDelegate.setDefaultNightMode(modes[w]); + }) + .show(); + } + + private String themeLabel() { + switch (prefs().getInt("k2go_theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)) { + case AppCompatDelegate.MODE_NIGHT_NO: return "Light"; + case AppCompatDelegate.MODE_NIGHT_YES: return "Dark"; + default: return "Follow system"; + } + } + + private void showAbout() { + new AlertDialog.Builder(requireContext()) + .setTitle("About") + .setMessage("Knowledge To Go (K2Go)\nVersion " + versionName() + + "\n" + requireContext().getPackageName()) + .setPositiveButton("OK", null) + .show(); + } + + private SharedPreferences prefs() { + return requireContext().getSharedPreferences( + getString(R.string.pref_file_internal), Context.MODE_PRIVATE); + } + private String versionName() { try { return "v" + requireContext().getPackageManager() @@ -57,6 +118,8 @@ private String versionName() { } } + // ---- rows ---- + private void header(String text) { TextView t = new TextView(requireContext()); t.setText(text); @@ -70,13 +133,26 @@ private void header(String text) { } private void row(String title, String value, View.OnClickListener onClick) { + buildRow(title, value, onClick, false); + } + + /** A visibly non-functional row: dimmed, tagged "Soon", not clickable. */ + private void preview(String title, String value) { + buildRow(title, (value == null || value.isEmpty()) ? "Soon" : value + " · Soon", null, true); + } + + private void buildRow(String title, String value, View.OnClickListener onClick, boolean isPreview) { LinearLayout row = new LinearLayout(requireContext()); row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); row.setBackgroundResource(R.drawable.k2go_card_bg); row.setPadding(dp(14), dp(14), dp(14), dp(14)); - row.setClickable(true); - row.setOnClickListener(onClick); + if (isPreview) { + row.setAlpha(0.5f); + } else { + row.setClickable(true); + row.setOnClickListener(onClick); + } TextView t = new TextView(requireContext()); t.setText(title); 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 f7dfbb7a..597d9ae7 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_clone.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -4,6 +4,9 @@ android:background="@color/k2go_paper" android:fillViewport="true"> + diff --git a/controller/app/src/main/res/layout/fragment_k2go_connect.xml b/controller/app/src/main/res/layout/fragment_k2go_connect.xml index 04633f00..4311a12b 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_connect.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_connect.xml @@ -4,6 +4,9 @@ android:background="@color/k2go_paper" android:fillViewport="true"> + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index a772e838..30ad67ce 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -97,4 +97,5 @@ With pictures Text only Summaries + Preview — not functional yet From 439f2cf544bdff427efd5184619a6ace22373e1d Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:06:36 -0600 Subject: [PATCH 40/55] ADFA-4725: Step 1 'Back' returns to the library, not the Android home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 1's Back did a bare finish(); since the wizard already finished when it launched Setup, that dropped the user to the home screen. Route to LibraryActivity (CLEAR_TOP | SINGLE_TOP so an existing instance is reused) — a consistent escape whether Setup was opened from the wizard or from inside the running library. --- .../iiab/controller/redesign/Step1SystemFragment.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java index aa22d9b7..b81dd6c7 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -81,7 +81,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } }); - root.findViewById(R.id.k2go_step1_back).setOnClickListener(v -> requireActivity().finish()); + root.findViewById(R.id.k2go_step1_back).setOnClickListener(v -> { + // Setup was already marked complete when the wizard launched this; route to the + // library (reusing it if it is under us) instead of a bare finish() that would + // drop the user to the Android home screen. + android.content.Intent i = new android.content.Intent(requireContext(), LibraryActivity.class); + i.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(i); + requireActivity().finish(); + }); root.findViewById(R.id.k2go_step1_skip_now).setOnClickListener(v -> { requireContext().getSharedPreferences(getString(R.string.pref_file_internal), android.content.Context.MODE_PRIVATE) .edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); From b0bcf2bb87ab440cd4e5b4e5f810ea0c58143c0f Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:31:03 -0600 Subject: [PATCH 41/55] =?UTF-8?q?ADFA-4725:=20Settings=20v3=20=E2=80=94=20?= =?UTF-8?q?collapsed=20list,=20sub-screens,=20pinned=20Turn=20off,=20funct?= =?UTF-8?q?ional=20Language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganizes the Settings tab to cut option overload and make Language work. - Top level: Language · Theme · Help · Send feedback · About · Advanced. "Turn off K2Go" is a pinned footer OUTSIDE the scroll, so it is never lost regardless of screen size or font scale; the minimize note sits above it and the fuller explanation is in the confirm dialog. - Advanced and About are sub-screens (child of the Settings tab, so the bottom nav stays; a ‹ back returns to the top level). Advanced is flat with inline SYSTEM / DEVELOPER subheaders. - Language is functional and sets BOTH the app UI locale (AppLocaleController + SupportedAppLanguages, which recreates and persists) AND the default content language (selected_lang_minimal, derived from the chosen tag). - Also wired: Theme (night-mode chooser), Send feedback (reuses FeedbackFragment), About → real version + Permissions (system settings) + Share usage statistics (AnalyticsConsent toggle). - Not-yet-implemented rows (Help, Advanced items, licenses, Privacy) render as dimmed "Soon" previews rather than rows that look active but do nothing. Shared SettingsUi builds the rows for the top level and the sub-screens. --- .../controller/redesign/LibraryActivity.java | 10 + .../controller/redesign/SettingsFragment.java | 186 +++++------------- .../redesign/SettingsSubFragment.java | 121 ++++++++++++ .../iiab/controller/redesign/SettingsUi.java | 120 +++++++++++ .../res/layout/fragment_k2go_settings.xml | 19 +- .../res/layout/fragment_k2go_settings_sub.xml | 22 +++ 6 files changed, 335 insertions(+), 143 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java create mode 100644 controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java create mode 100644 controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index b88bae9f..b3c1899e 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -154,11 +154,21 @@ private void showTab(int itemId) { } else { f = PlaceholderFragment.newInstance(title); } + // Switching tabs clears any Settings sub-screen on the back stack. + getSupportFragmentManager().popBackStack(null, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); getSupportFragmentManager().beginTransaction() .replace(R.id.k2go_nav_host, f) .commit(); } + /** Push a Settings sub-screen (Language/About/Advanced/Feedback) keeping the bottom nav. */ + public void openSettingsSub(androidx.fragment.app.Fragment f) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_nav_host, f) + .addToBackStack("settings_sub") + .commit(); + } + @Override protected void onResume() { super.onResume(); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index ffe6bff1..e0488f31 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -1,19 +1,14 @@ package org.iiab.controller.redesign; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.provider.Settings; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -21,54 +16,41 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import org.iiab.controller.R; +import org.iiab.controller.feedback.presentation.FeedbackFragment; -/** Settings tab (ADFA-4725): Basics (Language/Theme/About wired; rest marked as preview) + - * Advanced (preview) + Turn off K2Go. Preview rows are visibly non-functional so nothing - * looks like a working button that does nothing. */ +/** Settings v3 top level: Language · Theme · Help · Send feedback · About · Advanced, with a + * pinned "Turn off K2Go" footer that stays visible outside the scroll. Advanced/About/Language + * open as sub-screens (bottom nav stays); Theme + Language + Send feedback are functional. */ public class SettingsFragment extends Fragment { - private LinearLayout list; - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { View root = inflater.inflate(R.layout.fragment_k2go_settings, c, false); - list = root.findViewById(R.id.k2go_settings_list); - - header("BASICS"); - row("Language", "", v -> openLanguageSettings()); - preview("Permissions", ""); - row("Theme", themeLabel(), v -> chooseTheme()); - preview("Share usage statistics", "On"); - row("About", versionName(), v -> showAbout()); - preview("Send feedback", ""); - - header("ADVANCED · for power users"); - preview("System & modules", ""); - preview("Backups & recovery", ""); - preview("Developer tools (ADB)", ""); - preview("Network & DNS", ""); - preview("Terminal (Debian)", ""); - - turnOffRow(); + Context ctx = requireContext(); + LinearLayout list = root.findViewById(R.id.k2go_settings_list); + LinearLayout footer = root.findViewById(R.id.k2go_settings_footer); + + SettingsUi.row(ctx, list, "Language", null, null, v -> openSub("language")); + SettingsUi.row(ctx, list, "Theme", null, themeLabel(), v -> chooseTheme()); + SettingsUi.preview(ctx, list, "Help", null); + SettingsUi.row(ctx, list, "Send feedback", null, null, v -> openFeedback()); + SettingsUi.row(ctx, list, "About", null, versionName(), v -> openSub("about")); + SettingsUi.row(ctx, list, "Advanced", "System, backups, developer tools", null, v -> openSub("advanced")); + + buildFooter(ctx, footer); return root; } - // ---- wired actions ---- + private void openSub(String screen) { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).openSettingsSub(SettingsSubFragment.newInstance(screen)); + } + } - private void openLanguageSettings() { - try { - if (Build.VERSION.SDK_INT >= 33) { - startActivity(new Intent(Settings.ACTION_APP_LOCALE_SETTINGS, - Uri.parse("package:" + requireContext().getPackageName()))); - return; - } - } catch (Exception ignore) { /* fall through */ } - try { - startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:" + requireContext().getPackageName()))); - } catch (Exception e) { - Toast.makeText(requireContext(), "Language settings unavailable on this device", Toast.LENGTH_SHORT).show(); + private void openFeedback() { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).openSettingsSub(new FeedbackFragment()); } } @@ -95,113 +77,31 @@ private String themeLabel() { } } - private void showAbout() { - new AlertDialog.Builder(requireContext()) - .setTitle("About") - .setMessage("Knowledge To Go (K2Go)\nVersion " + versionName() - + "\n" + requireContext().getPackageName()) - .setPositiveButton("OK", null) - .show(); - } - - private SharedPreferences prefs() { - return requireContext().getSharedPreferences( - getString(R.string.pref_file_internal), Context.MODE_PRIVATE); - } - - private String versionName() { - try { - return "v" + requireContext().getPackageManager() - .getPackageInfo(requireContext().getPackageName(), 0).versionName; - } catch (Exception e) { - return ""; - } - } - - // ---- rows ---- - - private void header(String text) { - TextView t = new TextView(requireContext()); - t.setText(text); - t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); - t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal)); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - lp.topMargin = dp(18); - lp.bottomMargin = dp(4); - list.addView(t, lp); - } - - private void row(String title, String value, View.OnClickListener onClick) { - buildRow(title, value, onClick, false); - } - - /** A visibly non-functional row: dimmed, tagged "Soon", not clickable. */ - private void preview(String title, String value) { - buildRow(title, (value == null || value.isEmpty()) ? "Soon" : value + " · Soon", null, true); - } - - private void buildRow(String title, String value, View.OnClickListener onClick, boolean isPreview) { - LinearLayout row = new LinearLayout(requireContext()); - row.setOrientation(LinearLayout.HORIZONTAL); - row.setGravity(Gravity.CENTER_VERTICAL); - row.setBackgroundResource(R.drawable.k2go_card_bg); - row.setPadding(dp(14), dp(14), dp(14), dp(14)); - if (isPreview) { - row.setAlpha(0.5f); - } else { - row.setClickable(true); - row.setOnClickListener(onClick); - } - - TextView t = new TextView(requireContext()); - t.setText(title); - t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); - t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_ink)); - row.addView(t, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); - - TextView val = new TextView(requireContext()); - val.setText(value == null ? "" : value); - val.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); - val.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted)); - row.addView(val, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - lp.topMargin = dp(8); - list.addView(row, lp); - } - - private void turnOffRow() { - TextView note = new TextView(requireContext()); - note.setText("Home/back only minimize — the library keeps running. Turn off to fully stop it."); + private void buildFooter(Context ctx, LinearLayout footer) { + TextView note = new TextView(ctx); + note.setText("K2Go keeps running in the background until you turn it off."); note.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); - note.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted)); - LinearLayout.LayoutParams np = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - np.topMargin = dp(24); - list.addView(note, np); + note.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + LinearLayout.LayoutParams np = new LinearLayout.LayoutParams(-1, -2); + np.bottomMargin = SettingsUi.dp(ctx, 8); + footer.addView(note, np); - TextView off = new TextView(requireContext()); + TextView off = new TextView(ctx); off.setText("Turn off K2Go"); off.setGravity(Gravity.CENTER); - off.setPadding(dp(16), dp(16), dp(16), dp(16)); + off.setPadding(SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16)); off.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); - off.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_clay)); + off.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_clay)); off.setBackgroundResource(R.drawable.k2go_getmore_bg); off.setClickable(true); off.setOnClickListener(v -> confirmTurnOff()); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - lp.topMargin = dp(8); - list.addView(off, lp); + footer.addView(off, new LinearLayout.LayoutParams(-1, -2)); } private void confirmTurnOff() { new AlertDialog.Builder(requireContext()) .setTitle("Turn off K2Go?") - .setMessage("This stops the library and closes the app.") + .setMessage("This stops the library and closes the app. (Home or back only minimize it — it keeps running in the background.)") .setNegativeButton("Cancel", null) .setPositiveButton("Turn off", (d, w) -> { if (getActivity() instanceof LibraryActivity) { @@ -211,7 +111,17 @@ private void confirmTurnOff() { .show(); } - private int dp(int v) { - return Math.round(v * getResources().getDisplayMetrics().density); + private SharedPreferences prefs() { + return requireContext().getSharedPreferences( + getString(R.string.pref_file_internal), Context.MODE_PRIVATE); + } + + private String versionName() { + try { + return "v" + requireContext().getPackageManager() + .getPackageInfo(requireContext().getPackageName(), 0).versionName; + } catch (Exception e) { + return ""; + } } } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java new file mode 100644 index 00000000..9340c5af --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java @@ -0,0 +1,121 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import java.util.List; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.AppLocaleController; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.applang.domain.AppLanguage; +import org.iiab.controller.applang.domain.SupportedAppLanguages; +import org.iiab.controller.delivery.data.AnalyticsConsent; + +/** Settings sub-screen host: Language (functional — UI + content), About (version, permissions, + * usage-stats consent), Advanced (power-user features, preview for now). Keeps the bottom nav; + * the ‹ back returns to the Settings top level. */ +public class SettingsSubFragment extends Fragment { + + private static final String ARG = "screen"; + + static SettingsSubFragment newInstance(String screen) { + SettingsSubFragment f = new SettingsSubFragment(); + Bundle b = new Bundle(); + b.putString(ARG, screen); + f.setArguments(b); + return f; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_settings_sub, c, false); + Context ctx = requireContext(); + LinearLayout list = root.findViewById(R.id.k2go_sub_list); + TextView title = root.findViewById(R.id.k2go_sub_title); + root.findViewById(R.id.k2go_sub_back).setOnClickListener(v -> + requireActivity().getSupportFragmentManager().popBackStack()); + + String screen = getArguments() != null ? getArguments().getString(ARG, "") : ""; + switch (screen) { + case "language": title.setText("Language"); buildLanguage(ctx, list); break; + case "about": title.setText("About"); buildAbout(ctx, list); break; + case "advanced": title.setText("Advanced"); buildAdvanced(ctx, list); break; + default: title.setText("Settings"); + } + return root; + } + + // ---- Language: one choice sets BOTH the app UI locale and the content language ---- + private void buildLanguage(Context ctx, LinearLayout list) { + SettingsUi.caption(ctx, list, "Sets the app language and the default content language."); + List langs = SupportedAppLanguages.all(getString(R.string.setup_app_lang_system)); + String current = AppLocaleController.currentTag(); + for (AppLanguage lang : langs) { + boolean isCurrent = lang.tag().equals(current); + SettingsUi.row(ctx, list, lang.toString(), null, isCurrent ? "✓" : null, + v -> applyLanguage(ctx, lang.tag())); + } + } + + private void applyLanguage(Context ctx, String tag) { + // Content language = the base subtag (ru-RU -> ru), normalized to the Kiwix form; + // empty tag (follow system) -> derive from the system locale. + String content = (tag == null || tag.isEmpty()) + ? ContentLanguage.systemDefault() + : ContentLanguage.normalize(tag.split("-")[0]); + ctx.getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE) + .edit().putString("selected_lang_minimal", content).apply(); + // Applying the UI locale recreates the activities (AppCompat persists the choice). + AppLocaleController.apply(tag); + } + + // ---- About ---- + private void buildAbout(Context ctx, LinearLayout list) { + SettingsUi.infoRow(ctx, list, "App version", versionName(ctx)); + SettingsUi.row(ctx, list, "Permissions", null, null, v -> openAppSettings(ctx)); + SettingsUi.toggle(ctx, list, "Share usage statistics", AnalyticsConsent.isEnabled(ctx), checked -> { + AnalyticsConsent.setEnabled(ctx, checked); + org.iiab.controller.analytics.AnalyticsClient.with(ctx).applyConsent(); + }); + SettingsUi.preview(ctx, list, "Open-source licenses", null); + SettingsUi.preview(ctx, list, "Privacy", null); + } + + private void openAppSettings(Context ctx) { + try { + ctx.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:" + ctx.getPackageName()))); + } catch (Exception ignore) { /* no-op */ } + } + + // ---- Advanced (power-user features — preview for now) ---- + private void buildAdvanced(Context ctx, LinearLayout list) { + SettingsUi.caption(ctx, list, "For power users."); + SettingsUi.sectionHeader(ctx, list, "SYSTEM"); + SettingsUi.preview(ctx, list, "Module management", "Tier, add modules, hide"); + SettingsUi.preview(ctx, list, "Backups & recovery", null); + SettingsUi.sectionHeader(ctx, list, "DEVELOPER"); + SettingsUi.preview(ctx, list, "ADB", null); + SettingsUi.preview(ctx, list, "Network & DNS", null); + SettingsUi.preview(ctx, list, "Terminal (Debian)", null); + } + + private String versionName(Context ctx) { + try { + return "v" + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName; + } catch (Exception e) { + return ""; + } + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java new file mode 100644 index 00000000..e9a1d912 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java @@ -0,0 +1,120 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; +import org.iiab.controller.R; + +/** Programmatic row builders shared by the Settings top level and its sub-screens. */ +final class SettingsUi { + private SettingsUi() {} + + interface OnToggle { void changed(boolean checked); } + + static int dp(Context c, int v) { return Math.round(v * c.getResources().getDisplayMetrics().density); } + + static void sectionHeader(Context c, LinearLayout list, String text) { + TextView t = new TextView(c); + t.setText(text); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_teal)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-2, -2); + lp.topMargin = dp(c, 18); lp.bottomMargin = dp(c, 4); + list.addView(t, lp); + } + + static void caption(Context c, LinearLayout list, String text) { + TextView t = new TextView(c); + t.setText(text); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); + lp.bottomMargin = dp(c, 2); + list.addView(t, lp); + } + + static View row(Context c, LinearLayout list, String title, String subtitle, String value, View.OnClickListener onClick) { + LinearLayout row = baseRow(c, list); + if (onClick != null) { row.setClickable(true); row.setOnClickListener(onClick); } + LinearLayout col = new LinearLayout(c); + col.setOrientation(LinearLayout.VERTICAL); + col.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + col.addView(title(c, title)); + if (subtitle != null && !subtitle.isEmpty()) col.addView(sub(c, subtitle)); + row.addView(col); + if (value != null && !value.isEmpty()) { + TextView v = sub(c, value); + v.setPadding(0, 0, dp(c, 8), 0); + row.addView(v); + } + if (onClick != null) { + TextView chev = new TextView(c); + chev.setText("›"); + chev.setTextSize(18); + chev.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + row.addView(chev); + } + return row; + } + + static void preview(Context c, LinearLayout list, String title, String subtitle) { + LinearLayout row = baseRow(c, list); + row.setAlpha(0.5f); + LinearLayout col = new LinearLayout(c); + col.setOrientation(LinearLayout.VERTICAL); + col.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + col.addView(title(c, title)); + if (subtitle != null && !subtitle.isEmpty()) col.addView(sub(c, subtitle)); + row.addView(col); + row.addView(sub(c, "Soon")); + } + + static void toggle(Context c, LinearLayout list, String titleText, boolean checked, OnToggle cb) { + LinearLayout row = baseRow(c, list); + TextView t = title(c, titleText); + t.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + row.addView(t); + SwitchCompat sw = new SwitchCompat(c); + sw.setChecked(checked); + sw.setOnCheckedChangeListener((b, isChk) -> cb.changed(isChk)); + row.addView(sw); + } + + static void infoRow(Context c, LinearLayout list, String titleText, String value) { + LinearLayout row = baseRow(c, list); + TextView t = title(c, titleText); + t.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + row.addView(t); + row.addView(sub(c, value)); + } + + private static TextView title(Context c, String s) { + TextView t = new TextView(c); + t.setText(s); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_ink)); + return t; + } + private static TextView sub(Context c, String s) { + TextView t = new TextView(c); + t.setText(s); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + return t; + } + private static LinearLayout baseRow(Context c, LinearLayout list) { + LinearLayout row = new LinearLayout(c); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setBackgroundResource(R.drawable.k2go_card_bg); + row.setPadding(dp(c, 14), dp(c, 14), dp(c, 14), dp(c, 14)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); + lp.topMargin = dp(c, 8); + list.addView(row, lp); + return row; + } +} diff --git a/controller/app/src/main/res/layout/fragment_k2go_settings.xml b/controller/app/src/main/res/layout/fragment_k2go_settings.xml index 03e4d45b..ad548b3e 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_settings.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_settings.xml @@ -1,7 +1,16 @@ - - - + android:orientation="vertical" android:background="@color/k2go_paper"> + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml b/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml new file mode 100644 index 00000000..18d01f70 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + From 361bde7228ab62284a2e330074ec2f242e18a879 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:42:23 -0600 Subject: [PATCH 42/55] ADFA-4725: fix doubled 'v' in Settings app version (versionName already starts with 'v') --- .../java/org/iiab/controller/redesign/SettingsFragment.java | 2 +- .../java/org/iiab/controller/redesign/SettingsSubFragment.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index e0488f31..45bae84a 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -118,7 +118,7 @@ private SharedPreferences prefs() { private String versionName() { try { - return "v" + requireContext().getPackageManager() + return requireContext().getPackageManager() .getPackageInfo(requireContext().getPackageName(), 0).versionName; } catch (Exception e) { return ""; diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java index 9340c5af..1fd36bec 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java @@ -113,7 +113,7 @@ private void buildAdvanced(Context ctx, LinearLayout list) { private String versionName(Context ctx) { try { - return "v" + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName; + return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName; } catch (Exception e) { return ""; } From dda6a95625f4cd9f6bfb4fc68a35995af2964643 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:45:51 -0600 Subject: [PATCH 43/55] ADFA-4725: wire Advanced > Terminal (Debian) to the existing full terminal The Debian terminal already exists (TerminalController + TerminalView hosted in MainActivity). Opening it is a single intent: launch MainActivity with EXTRA_OPEN_TERMINAL, which runs maybeOpenTerminalFromIntent -> openFullTerminal. Rest of Advanced stays preview. --- .../iiab/controller/redesign/SettingsSubFragment.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java index 1fd36bec..777926d6 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java @@ -99,6 +99,13 @@ private void openAppSettings(Context ctx) { } catch (Exception ignore) { /* no-op */ } } + /** The full Debian terminal lives in MainActivity; EXTRA_OPEN_TERMINAL opens it directly. */ + private void openTerminal(Context ctx) { + Intent i = new Intent(ctx, org.iiab.controller.MainActivity.class); + i.putExtra(org.iiab.controller.MainActivity.EXTRA_OPEN_TERMINAL, true); + ctx.startActivity(i); + } + // ---- Advanced (power-user features — preview for now) ---- private void buildAdvanced(Context ctx, LinearLayout list) { SettingsUi.caption(ctx, list, "For power users."); @@ -108,7 +115,7 @@ private void buildAdvanced(Context ctx, LinearLayout list) { SettingsUi.sectionHeader(ctx, list, "DEVELOPER"); SettingsUi.preview(ctx, list, "ADB", null); SettingsUi.preview(ctx, list, "Network & DNS", null); - SettingsUi.preview(ctx, list, "Terminal (Debian)", null); + SettingsUi.row(ctx, list, "Terminal (Debian)", null, null, v -> openTerminal(ctx)); } private String versionName(Context ctx) { From 80d95fde9193768dae23d76176a0662b9020a93d Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:53:29 -0600 Subject: [PATCH 44/55] =?UTF-8?q?ADFA-4725:=20terminal=20opens=20in=20"ter?= =?UTF-8?q?minal-only"=20mode=20=E2=80=94=20no=20leak=20into=20the=20old?= =?UTF-8?q?=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening Advanced > Terminal launched MainActivity to host the terminal sheet, but hiding/closing the sheet revealed the old MainActivity dashboard (ESTADO/USO/…), dropping the user into the previous UI. New EXTRA_TERMINAL_ONLY flag: when MainActivity is launched just for the terminal (from the new UI), a BottomSheet callback finishes the activity as soon as the terminal sheet is hidden, returning to the new UI instead of exposing the old dashboard. The redesign row sends the flag alongside EXTRA_OPEN_TERMINAL. --- .../org/iiab/controller/MainActivity.java | 34 +++++++++++++++++-- .../redesign/SettingsSubFragment.java | 1 + 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/controller/app/src/main/java/org/iiab/controller/MainActivity.java index 6db46524..c57cb6c8 100644 --- a/controller/app/src/main/java/org/iiab/controller/MainActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -111,6 +111,11 @@ public boolean isTermuxInstalled() { /** 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() { @@ -581,10 +586,35 @@ protected void onNewIntent(Intent intent) { 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); - if (root != null) root.post(() -> terminalController.openFullTerminal()); - else terminalController.openFullTerminal(); + Runnable open = () -> { + terminalController.openFullTerminal(); + if (terminalOnlyMode) attachTerminalOnlyFinish(); + }; + 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); + 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() { diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java index 777926d6..74798efd 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java @@ -103,6 +103,7 @@ private void openAppSettings(Context ctx) { private void openTerminal(Context ctx) { Intent i = new Intent(ctx, org.iiab.controller.MainActivity.class); i.putExtra(org.iiab.controller.MainActivity.EXTRA_OPEN_TERMINAL, true); + i.putExtra(org.iiab.controller.MainActivity.EXTRA_TERMINAL_ONLY, true); ctx.startActivity(i); } From 37d8c66f0b2c76cfc18223ee3e2c8ec37a3573aa Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 17:57:41 -0600 Subject: [PATCH 45/55] ADFA-4725: terminal keep-alive notification also opens terminal-only The 'Terminal K2Go activa' notification opened MainActivity in normal mode, showing the old dashboard behind the sheet. It now carries EXTRA_TERMINAL_ONLY too, so both entry points (Advanced > Terminal and the notification tap) finish back to the new UI when the terminal is hidden instead of exposing the old dashboard. onNewIntent already routes to maybeOpenTerminalFromIntent, so SINGLE_TOP reuse is covered. --- .../main/java/org/iiab/controller/TerminalSessionService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java b/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java index 3797b4d9..e7a795e3 100644 --- a/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java +++ b/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java @@ -97,6 +97,7 @@ private Notification buildNotification() { // version-footer gesture (ADFA-4696). SINGLE_TOP reuses the running Activity. Intent open = new Intent(this, MainActivity.class) .putExtra(MainActivity.EXTRA_OPEN_TERMINAL, true) + .putExtra(MainActivity.EXTRA_TERMINAL_ONLY, true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pending = PendingIntent.getActivity( this, 0, open, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); From 62d8b27ff2110a4dfb0499e6a7780fedd976ce31 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 18:25:26 -0600 Subject: [PATCH 46/55] =?UTF-8?q?ADFA-4725:=20non-destructive=20"Get=20mor?= =?UTF-8?q?e"=20=E2=80=94=20never=20overwrite=20an=20installed=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding content must not wipe the OS or the user's customized content. The system layer (Basic/Standard/Full precompiled blocks) is destructive to re-extract; Kiwix/Maps content is additive. - InstallService guard: if a rootfs is already installed and no explicit reinstall was requested, the pipeline skips the OS download/extract entirely and only runs the additive companion-data steps (or no-ops). Fresh installs and explicit reinstalls are unchanged. - Get more is state-aware: with a system already installed (detected on disk via SystemStateEvaluator.isSystemInstalled), it opens Step 2 (content) directly and skips the system step; with no system it runs the full setup from Step 1. - The installed tier is persisted at install time and read back when entering content-only, so Step 2 sizes content against the real tier. - Step 2 "Back" returns to the library when it is the entry point (content-only), instead of a no-op. The deeper separation of the software vs content installers (and an explicit, guarded change-system flow) is deferred to a follow-up. --- .../iiab/controller/SystemStateEvaluator.java | 8 +++++++ .../install/presentation/InstallService.java | 18 ++++++++++++++ .../redesign/LibraryHomeFragment.java | 11 +++++++-- .../redesign/SetupLibraryActivity.java | 24 ++++++++++++++++++- .../redesign/Step2OptionAFragment.java | 12 +++++++--- .../redesign/Step2OptionBFragment.java | 7 ++++-- 6 files changed, 72 insertions(+), 8 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java b/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java index d81b3691..8a2cd98b 100644 --- a/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java +++ b/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java @@ -26,6 +26,14 @@ private SystemStateEvaluator() { private static volatile String cachedDebianArch; private static volatile boolean archCalculated; + /** True when a system (rootfs) is actually installed on disk — the reliable signal for + * whether "Get more" should skip the destructive system step and go straight to content. */ + public static boolean isSystemInstalled(Context ctx) { + File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab"); + return new File(rootfsDir, "bin/bash").exists() + || new File(rootfsDir, "usr/local/pdsm/flag_install_ready").exists(); + } + /** Server responding → ONLINE; else derive from the rootfs on disk. */ public static DashboardFragment.SystemState evaluate(Context ctx, boolean serverAlive) { File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab"); diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java index c32b5707..58e6ef0a 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java @@ -194,8 +194,25 @@ public int onStartCommand(Intent intent, int flags, int startId) { // ---------------------------------------------------------------- pipeline + /** Record the tier being installed so a later content-only "Get more" can size correctly. */ + private void persistInstalledTier() { + try { + getSharedPreferences(getString(R.string.pref_file_internal), android.content.Context.MODE_PRIVATE) + .edit().putString("installed_tier", tier.name()).apply(); + } catch (Exception ignore) { /* best-effort */ } + } + private void runPipeline() { try { + // Non-destructive guard (ADFA-4725): an already-installed system is NEVER + // re-extracted unless an explicit reinstall was requested. "Get more" then only + // runs the additive companion-data steps (Kiwix zims + Maps) inside the existing + // rootfs, so the OS blocks and any customized content are left untouched. + if (!reinstall && debianRootfs.exists() && debianRootfs.isDirectory()) { + if (companionData) startCompanionData(); + else finishSuccess(); + return; + } if (reinstall && debianRootfs.exists() && debianRootfs.isDirectory()) { postProvisioning(getString(R.string.install_status_wiping_old)); try { @@ -208,6 +225,7 @@ private void runPipeline() { } } if (cancelled) return; + persistInstalledTier(); startRootfsDownload(); } catch (Exception e) { Log.e(TAG, "Install pipeline crashed", e); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java index dcf55f0b..d4698f92 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java @@ -70,8 +70,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c buildCards(inflater, root.findViewById(R.id.k2go_cards)); - root.findViewById(R.id.k2go_get_more).setOnClickListener(v -> - startActivity(new android.content.Intent(requireContext(), SetupLibraryActivity.class))); + root.findViewById(R.id.k2go_get_more).setOnClickListener(v -> { + // If a system is already installed, skip the destructive system step and go + // straight to content (Step 2). Otherwise run the full setup from Step 1. + android.content.Intent i = new android.content.Intent(requireContext(), SetupLibraryActivity.class); + if (org.iiab.controller.SystemStateEvaluator.isSystemInstalled(requireContext())) { + i.putExtra(SetupLibraryActivity.EXTRA_CONTENT_ONLY, true); + } + startActivity(i); + }); return root; } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java index 8a0ba8c6..19bf52f4 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -15,6 +15,10 @@ public class SetupLibraryActivity extends AppCompatActivity { private InstallationPlanner.Tier selectedTier = InstallationPlanner.Tier.STANDARD; + + /** Launch extra: skip Step 1 (system) and open Step 2 (content) directly, for when a + * system is already installed so adding content never overwrites it. */ + public static final String EXTRA_CONTENT_ONLY = "contentOnly"; private boolean contentEverything = false; // legacy (kept for compat; unused by the picker) private boolean contentPictures = true; // legacy private boolean optionB = false; // A is the default @@ -28,8 +32,16 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_k2go_setup); if (savedInstanceState == null) { + boolean contentOnly = getIntent().getBooleanExtra(EXTRA_CONTENT_ONLY, false); + androidx.fragment.app.Fragment first; + if (contentOnly) { + selectedTier = readInstalledTier(); // size content against the installed tier + first = step2Fragment(); + } else { + first = new Step1SystemFragment(); + } getSupportFragmentManager().beginTransaction() - .replace(R.id.k2go_setup_host, new Step1SystemFragment()) + .replace(R.id.k2go_setup_host, first) .commit(); } } @@ -48,6 +60,16 @@ protected void onCreate(Bundle savedInstanceState) { public String getWikiView() { return wikiView; } public void setWikiView(String v) { wikiView = v; } + private InstallationPlanner.Tier readInstalledTier() { + String t = getSharedPreferences(getString(R.string.pref_file_internal), MODE_PRIVATE) + .getString("installed_tier", InstallationPlanner.Tier.STANDARD.name()); + try { + return InstallationPlanner.Tier.valueOf(t); + } catch (Exception e) { + return InstallationPlanner.Tier.STANDARD; + } + } + private Fragment step2Fragment() { return optionB ? new Step2OptionBFragment() : new Step2OptionAFragment(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index 3a3f6a82..b0a1941d 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -100,9 +100,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mapsCheck.setOnClickListener(x -> mapsCheck.setChecked(true)); // fixed download.setOnClickListener(x -> startDownload()); - v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> { - if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); - }); + v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> backOrExit()); InstallationPlanner.calculateProjectedSize(requireContext(), tier(), false, lang, null, new InstallationPlanner.PlanResultListener() { @@ -178,6 +176,14 @@ private void applyBar(double systemGb, double picksGb) { legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Your picks %.1f · Free %.1f", used, systemGb, picksGb, freeAfter)); } + /** Back to Step 1 when it is on the stack; otherwise (content-only entry) return to the library. */ + private void backOrExit() { + if (getActivity() == null) return; + androidx.fragment.app.FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm.getBackStackEntryCount() > 0) fm.popBackStack(); + else getActivity().finish(); + } + private void startDownload() { if (wikiInvalid()) { Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); return; } String variant = wikiInc ? WikiVariants.primary(sel) : null; diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index cb203e73..058b02b9 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -91,8 +91,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c refreshProjection(); }); btnBack.setOnClickListener(x -> { - if (step > 0) { step--; render(); } - else if (getActivity() != null) getActivity().getSupportFragmentManager().popBackStack(); + if (step > 0) { step--; render(); return; } + if (getActivity() == null) return; + androidx.fragment.app.FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm.getBackStackEntryCount() > 0) fm.popBackStack(); + else getActivity().finish(); }); btnSkip.setOnClickListener(x -> { if (step == 0) { From 5f4c68b80bccb69e33d1e1aa7180e020cddf7f66 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 18:47:14 -0600 Subject: [PATCH 47/55] =?UTF-8?q?ADFA-4725:=20skipping=20Wikipedia=20means?= =?UTF-8?q?=20zero=20=E2=80=94=20and=20align=20Option=20B=20breadcrumb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Step-2 fixes. - Skipping Wikipedia no longer pulls in a tier-coupled preset. The UI passed a null variant when Wikipedia was skipped, and computeProjection treats null as "legacy auto-pick" — it ran the priority list and returned a large ZIM. Option B's gauge showed that phantom size (~11 GB), and worse, InstallService would actually download that auto-picked ZIM even though the user skipped Wikipedia. Now "skip" sends an explicit empty sentinel that computeProjection resolves to zero Kiwix (no size, no download); null still means legacy auto-pick for old callers. - Option B breadcrumb now matches Option A / Step 1 ("1 ✓ System → 2 Content") instead of the divergent "System ✓ · Content — step 2 of 2". --- .../main/java/org/iiab/controller/InstallationPlanner.java | 5 ++++- .../org/iiab/controller/redesign/Step2OptionAFragment.java | 2 +- .../org/iiab/controller/redesign/Step2OptionBFragment.java | 4 ++-- controller/app/src/main/res/values/strings_k2go.xml | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java index bce1a65c..867b696a 100644 --- a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java +++ b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java @@ -289,7 +289,10 @@ public void onReady(JSONObject catalog) { } if (langData != null) { - if (overrideVariant != null) { + if (overrideVariant != null && overrideVariant.isEmpty()) { + // Explicit "no Wikipedia" (skip): leave kiwixSize 0 / resolvedFile + // null. Skipping must mean ZERO — never a tier-coupled auto-pick. + } else if (overrideVariant != null) { // Explicit pick from the UI. Honor it exactly, or degrade WITHIN // the same coverage family (e.g. top1m_maxi -> top_maxi), NEVER // escalating — a "Popular" pick must not silently become the full diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index b0a1941d..a8fc8d2c 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -186,7 +186,7 @@ private void backOrExit() { private void startDownload() { if (wikiInvalid()) { Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); return; } - String variant = wikiInc ? WikiVariants.primary(sel) : null; + String variant = (wikiInc && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; boolean companion = variant != null || booksInc || mapsInc; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 058b02b9..b51c7438 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -167,7 +167,7 @@ private void updateNextLabel() { } private void refreshProjection() { - String variant = wikiIncluded ? WikiVariants.primary(sel) : null; + String variant = (wikiIncluded && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variant, new InstallationPlanner.PlanResultListener() { @Override @@ -201,7 +201,7 @@ private void updateGauge(double systemGb, double picksGb) { } private void startDownload() { - String variant = wikiIncluded ? WikiVariants.primary(sel) : null; + String variant = (wikiIncluded && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); i.putExtra(InstallService.EXTRA_TIER, tier().name()); diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 30ad67ce..72509441 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -46,7 +46,7 @@ Storage access Storage System default - System ✓ · Content — step 2 of 2 + 1 ✓ System → 2 Content 1 ✓ System → 2 Content Text only The K2Go library on the other phone (if any) is replaced with this one. From 771c50378051659a7caa2f2bfbce3b409db6431a Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 18:51:24 -0600 Subject: [PATCH 48/55] ADFA-4725: Settings toggle row no longer oversized (cap the switch height) SwitchCompat enforces a ~48dp touch-target min-height, which made the 'Share usage statistics' row noticeably taller than the rest. Drop the switch's min-height and cap it so the toggle row lines up with the value rows. --- .../main/java/org/iiab/controller/redesign/SettingsUi.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java index e9a1d912..402909ed 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java @@ -80,8 +80,13 @@ static void toggle(Context c, LinearLayout list, String titleText, boolean check row.addView(t); SwitchCompat sw = new SwitchCompat(c); sw.setChecked(checked); + // The default switch enforces a 48dp touch-target min-height that inflates the row. + // Drop it and cap the switch so the toggle row matches the value rows. + sw.setMinimumHeight(0); + sw.setPadding(0, 0, 0, 0); sw.setOnCheckedChangeListener((b, isChk) -> cb.changed(isChk)); - row.addView(sw); + row.addView(sw, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, dp(c, 28))); } static void infoRow(Context c, LinearLayout list, String titleText, String value) { From d581ab0bc26d30e47672e77c7069d89c901d928e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 18:54:46 -0600 Subject: [PATCH 49/55] =?UTF-8?q?ADFA-4725:=20Option=20A=20=E2=80=94=20Wik?= =?UTF-8?q?ipedia=20starts=20collapsed=20like=20the=20other=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inclusion forced the Wikipedia card open on load, so users saw a wall of version options instead of the row of collapsible sources, missing the tap-to-expand pattern. Decouple inclusion from expansion: the card starts collapsed (as Books / Maps / Courses do) and only expands on tap; skipping still collapses it. --- .../org/iiab/controller/redesign/Step2OptionAFragment.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index a8fc8d2c..cec26c57 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -135,8 +135,9 @@ private void setWiki(boolean inc) { wikiCheck.setChecked(inc); wikiCard.setAlpha(inc ? 1f : 0.55f); wikiSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); - if (inc) { wikiBody.setVisibility(View.VISIBLE); wikiChevron.setText("▾"); } - else { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } + // Inclusion no longer forces the card open — Wikipedia starts collapsed like the + // other sources so the "tap to expand" pattern is visible. Skipping still collapses. + if (!inc) { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } if (act() != null) act().setWikiIncluded(inc); refresh(); } From 702ef7158055b79b19b41e34e3562a33f70b2f98 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 18:58:24 -0600 Subject: [PATCH 50/55] =?UTF-8?q?ADFA-4725:=20Library=20=E2=80=94=20pin=20?= =?UTF-8?q?'+=20Get=20more'=20as=20a=20footer=20so=20it=20never=20scrolls?= =?UTF-8?q?=20out=20of=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On small screens the Get more button scrolled off the bottom, so users couldn't tell there was a way to add more content. Same fix as Turn off in Settings: the cards scroll in a weighted ScrollView and Get more sits in a pinned footer outside the scroll, always visible. --- .../main/res/layout/fragment_k2go_library.xml | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/controller/app/src/main/res/layout/fragment_k2go_library.xml b/controller/app/src/main/res/layout/fragment_k2go_library.xml index f7898842..e1a73c4c 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_library.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_library.xml @@ -1,59 +1,76 @@ - + android:orientation="vertical" + android:background="@color/k2go_paper"> - - - + android:layout_height="0dp" + android:layout_weight="1" + android:fillViewport="true"> - - + android:orientation="vertical" + android:padding="16dp"> + android:text="@string/k2go_brand" + android:textAppearance="?attr/textAppearanceHeadlineMedium" + android:textColor="@color/k2go_ink" /> + + + + + + + + + + - + + - + From a5bcf0f09a7556696cca202211dd028a7dfcbc89 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 19:06:08 -0600 Subject: [PATCH 51/55] ADFA-4725: short boot gate when nothing is installed (no system to wait for) The entry animation waits for the proot/server to come up. If the user skipped install there is no rootfs/server, so the server never reports alive and the gate burned the full 25s safety timeout before revealing the library. Detect the no-system case (SystemStateEvaluator.isSystemInstalled): skip the server autostart kick and dismiss the gate after a short beat (~0.9s) instead of 25s, so a skipped/empty install opens promptly. A real install is unchanged. --- .../controller/redesign/LibraryActivity.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index b3c1899e..2cd92c1d 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -29,6 +29,8 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll private static final String TAG = "K2Go-Library"; private static final long AUTOSTART_DELAY_MS = 3500L; private static final long GATE_SAFETY_MS = 25000L; + /** Nothing installed → nothing to boot: dismiss the gate promptly instead of waiting. */ + private static final long NO_SYSTEM_GATE_MS = 900L; private ServerController serverController; private boolean isNegotiating = false; @@ -82,6 +84,10 @@ public android.graphics.Typeface fetchFont(String fontFamily) { bootGate.playAnimation(); } + // If the user skipped install there is no rootfs/server to wait for; the gate would + // otherwise burn the full safety timeout. Detect it and dismiss quickly. + final boolean systemInstalled = org.iiab.controller.SystemStateEvaluator.isSystemInstalled(this); + serverController = new ServerController(this, this); serverController.start(); @@ -96,19 +102,21 @@ public android.graphics.Typeface fetchFont(String fontFamily) { Handler main = new Handler(Looper.getMainLooper()); // If the stack isn't up after one poll cycle, start it. - main.postDelayed(() -> { - if (!isFinishing() - && !ServerStateRepository.get().current().alive - && targetServerState == null) { - serverController.handleServerLaunchClick(findViewById(android.R.id.content)); - } - }, AUTOSTART_DELAY_MS); + if (systemInstalled) { + main.postDelayed(() -> { + if (!isFinishing() + && !ServerStateRepository.get().current().alive + && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } + }, AUTOSTART_DELAY_MS); + } // Safety: never trap the user behind the gate. main.postDelayed(() -> { if (!gateDismissed) { onServerReady(); } - }, GATE_SAFETY_MS); + }, systemInstalled ? GATE_SAFETY_MS : NO_SYSTEM_GATE_MS); } private void onServerReady() { From 2a27d8afb88fa8f40125b9e23102fedb66d0dc90 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 19:15:12 -0600 Subject: [PATCH 52/55] ADFA-4725: entry gate waits for the install and shows real progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pressing "Download" started the 2-3 GB install in the background but immediately opened the library; the boot gate then dismissed on its timeout, dropping the user into the app long before the download finished and taking the nice progress bar with it. The Setup "Download" now flags the library launch as installing. In that mode the boot gate: - keeps looping (no timeout dismissal), - shows live progress over the animation — "Downloading your library… N% · speed" for the download, then an indeterminate bar for extract/provision, - and only flips open and reveals the app when the install reaches SUCCESS (or FAILED, so the user is never trapped). Driven by InstallProgressRepository/InstallState; an explicit EXTRA_INSTALLING flag avoids any race on whether the service has started yet. --- .../controller/redesign/LibraryActivity.java | 73 +++++++++++++++---- .../redesign/Step2OptionAFragment.java | 3 +- .../redesign/Step2OptionBFragment.java | 3 +- .../src/main/res/layout/activity_library.xml | 42 +++++++++++ 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index 2cd92c1d..c0ecf5f9 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -17,6 +17,8 @@ import org.iiab.controller.ServerController; import org.iiab.controller.ServerStateRepository; import org.iiab.controller.WatchdogService; +import org.iiab.controller.install.presentation.InstallProgressRepository; +import org.iiab.controller.install.presentation.InstallState; /** * New content-first UI shell (ADFA-4725). Owns the server lifecycle for the new UI: @@ -31,12 +33,18 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll private static final long GATE_SAFETY_MS = 25000L; /** Nothing installed → nothing to boot: dismiss the gate promptly instead of waiting. */ private static final long NO_SYSTEM_GATE_MS = 900L; + /** Set by the Setup "Download" so the gate waits for the install to finish, not a timeout. */ + public static final String EXTRA_INSTALLING = "installing"; + private boolean installing = false; private ServerController serverController; private boolean isNegotiating = false; private Boolean targetServerState = null; private LottieAnimationView bootGate; + private View installProgress; + private android.widget.TextView installStatus, installDetail; + private android.widget.ProgressBar installBar; private boolean gateDismissed = false; private boolean closing = false; private boolean closedDone = false; @@ -66,6 +74,11 @@ protected void onCreate(Bundle savedInstanceState) { } bootGate = findViewById(R.id.k2go_boot_gate); + installProgress = findViewById(R.id.k2go_install_progress); + installStatus = findViewById(R.id.k2go_install_status); + installBar = findViewById(R.id.k2go_install_bar); + installDetail = findViewById(R.id.k2go_install_detail); + installing = getIntent().getBooleanExtra(EXTRA_INSTALLING, false); // The Lottie has a text layer (OPEN/CLOSED sign) referencing "Atkinson Hyperlegible". // Without this delegate Lottie looks for assets/fonts/Atkinson Hyperlegible.ttf and // crashes (Font asset not found). Feed it the app's Atkinson font resource instead. @@ -100,23 +113,56 @@ public android.graphics.Typeface fetchFont(String fontFamily) { } }); + // Keep the gate up while an install runs, showing real progress, and dismiss only + // when it actually finishes (or fails) — a 2-3 GB download won't beat a timeout. + InstallProgressRepository.get().state().observe(this, st -> { + if (st == null || gateDismissed) return; + if (st.isRunning()) { installing = true; showInstallProgress(st); } + else if (st.isTerminal()) { hideInstallProgress(); onServerReady(); } + }); + Handler main = new Handler(Looper.getMainLooper()); - // If the stack isn't up after one poll cycle, start it. - if (systemInstalled) { + if (installing) { + // A download is in progress: keep the gate and show live progress; dismissal + // comes from the install reaching SUCCESS/FAILED, not a timeout. + showInstallProgress(InstallProgressRepository.get().current()); + } else { + // If the stack isn't up after one poll cycle, start it. + if (systemInstalled) { + main.postDelayed(() -> { + if (!isFinishing() + && !ServerStateRepository.get().current().alive + && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } + }, AUTOSTART_DELAY_MS); + } + // Safety: never trap the user behind the gate. main.postDelayed(() -> { - if (!isFinishing() - && !ServerStateRepository.get().current().alive - && targetServerState == null) { - serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + if (!gateDismissed) { + onServerReady(); } - }, AUTOSTART_DELAY_MS); + }, systemInstalled ? GATE_SAFETY_MS : NO_SYSTEM_GATE_MS); } - // Safety: never trap the user behind the gate. - main.postDelayed(() -> { - if (!gateDismissed) { - onServerReady(); - } - }, systemInstalled ? GATE_SAFETY_MS : NO_SYSTEM_GATE_MS); + } + + private void showInstallProgress(InstallState st) { + if (installProgress == null || st == null || !st.isRunning()) return; + installProgress.setVisibility(View.VISIBLE); + if (st.phase == InstallState.Phase.DOWNLOADING) { + installStatus.setText("Downloading your library…"); + installBar.setIndeterminate(false); + installBar.setProgress(st.percent); + installDetail.setText(st.percent + "%" + (st.speed.isEmpty() ? "" : " · " + st.speed)); + } else { + installStatus.setText(st.message.isEmpty() ? "Setting up your library…" : st.message); + installBar.setIndeterminate(true); + installDetail.setText(""); + } + } + + private void hideInstallProgress() { + if (installProgress != null) installProgress.setVisibility(View.GONE); } private void onServerReady() { @@ -124,6 +170,7 @@ private void onServerReady() { return; } gateDismissed = true; + hideInstallProgress(); if (reduceMotion()) { bootGate.setVisibility(View.GONE); return; } bootGate.removeAllAnimatorListeners(); bootGate.setRepeatCount(0); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index cec26c57..bcd8435f 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -200,7 +200,8 @@ private void startDownload() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); Toast.makeText(requireContext(), companion ? "Downloading your library…" : "Installing the system…", Toast.LENGTH_LONG).show(); - startActivity(new Intent(requireContext(), LibraryActivity.class)); + startActivity(new Intent(requireContext(), LibraryActivity.class) + .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); requireActivity().finish(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index b51c7438..a907e623 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -213,7 +213,8 @@ private void startDownload() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); Toast.makeText(requireContext(), "Downloading your library…", Toast.LENGTH_LONG).show(); - startActivity(new Intent(requireContext(), LibraryActivity.class)); + startActivity(new Intent(requireContext(), LibraryActivity.class) + .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); requireActivity().finish(); } } diff --git a/controller/app/src/main/res/layout/activity_library.xml b/controller/app/src/main/res/layout/activity_library.xml index 5bc27aa7..bbe1086d 100644 --- a/controller/app/src/main/res/layout/activity_library.xml +++ b/controller/app/src/main/res/layout/activity_library.xml @@ -37,4 +37,46 @@ android:clickable="true" android:focusable="true" android:scaleType="centerCrop" /> + + + + + + + + + + From 3f70d8bb5cd6adb38a8e5e873bf5e8c2c84f2e86 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 19:16:41 -0600 Subject: [PATCH 53/55] ADFA-4725: Turn off K2Go frame is clay, matching its text (not the teal Get-more border) Turn off reused the teal-outlined get-more background, so its clay text sat in a blue frame. Add a dedicated clay-stroked background for the destructive action and leave Get more's teal border untouched. --- .../java/org/iiab/controller/redesign/SettingsFragment.java | 2 +- controller/app/src/main/res/drawable/k2go_turnoff_bg.xml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 controller/app/src/main/res/drawable/k2go_turnoff_bg.xml diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index 45bae84a..a0b8ec5b 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -92,7 +92,7 @@ private void buildFooter(Context ctx, LinearLayout footer) { off.setPadding(SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16)); off.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); off.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_clay)); - off.setBackgroundResource(R.drawable.k2go_getmore_bg); + off.setBackgroundResource(R.drawable.k2go_turnoff_bg); off.setClickable(true); off.setOnClickListener(v -> confirmTurnOff()); footer.addView(off, new LinearLayout.LayoutParams(-1, -2)); diff --git a/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml b/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml new file mode 100644 index 00000000..6871427e --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml @@ -0,0 +1,6 @@ + + + + + + From 26c0e81618986399d7fba499433912e7ce5f1dc3 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 19:23:15 -0600 Subject: [PATCH 54/55] ADFA-4725: drop the redundant 'Downloading your library' toast on Download The Setup Download showed a LENGTH_LONG toast that overlapped the new persistent progress overlay for the first few seconds (two bars at once). The overlay now owns that message, so remove the toast. --- .../java/org/iiab/controller/redesign/Step2OptionAFragment.java | 1 - .../java/org/iiab/controller/redesign/Step2OptionBFragment.java | 1 - 2 files changed, 2 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index bcd8435f..db7d413d 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -199,7 +199,6 @@ private void startDownload() { i.putExtra(InstallService.EXTRA_REINSTALL, false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); - Toast.makeText(requireContext(), companion ? "Downloading your library…" : "Installing the system…", Toast.LENGTH_LONG).show(); startActivity(new Intent(requireContext(), LibraryActivity.class) .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); requireActivity().finish(); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index a907e623..30f125b4 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -212,7 +212,6 @@ private void startDownload() { i.putExtra(InstallService.EXTRA_REINSTALL, false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); - Toast.makeText(requireContext(), "Downloading your library…", Toast.LENGTH_LONG).show(); startActivity(new Intent(requireContext(), LibraryActivity.class) .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); requireActivity().finish(); From 6f09a43d4dec92159e2c8f01d370d820043624a5 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 17 Jul 2026 19:32:52 -0600 Subject: [PATCH 55/55] ADFA-4725: disable Maps install in the content flow (it ships with the system) Maps comes bundled in the software block (the tier's base image), so reinstalling it from the content step is redundant, and there is no way to customize it yet. Disable it for now: - Step 2 (A and B) no longer counts Maps in the projected size, and Option A shows the Maps card as "Included" (disabled), not a selectable download. - The Setup Download passes EXTRA_SKIP_MAPS; InstallService then bypasses the maps reinstall (runMapsAnsible) and goes straight to success. Scoped to the content flow via the flag, so the old UI's behavior is unchanged. --- .../controller/install/presentation/InstallService.java | 8 ++++++-- .../iiab/controller/redesign/Step2OptionAFragment.java | 9 ++++++--- .../iiab/controller/redesign/Step2OptionBFragment.java | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java index 58e6ef0a..41984370 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java @@ -83,6 +83,7 @@ public final class InstallService extends Service { // Which pipeline to run (ADFA-4476). Absent/"install" -> normal install; "reset" -> scratch reset. public static final String EXTRA_MODE = "mode"; + public static final String EXTRA_SKIP_MAPS = "skipMaps"; // content flow: maps ship in the base image public static final String MODE_INSTALL = "install"; public static final String MODE_RESET = "reset"; @@ -103,6 +104,7 @@ public final class InstallService extends Service { private String overrideKiwixLang; private String overrideKiwixVariant; private boolean reinstall; + private boolean skipMaps; private boolean resetMode; // ADFA-4476 slice 3: per-module install queue state (module mode only). @@ -169,6 +171,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { overrideKiwixLang = intent.getStringExtra(EXTRA_KIWIX_LANG); overrideKiwixVariant = intent.getStringExtra(EXTRA_KIWIX_VARIANT); reinstall = intent.getBooleanExtra(EXTRA_REINSTALL, false); + skipMaps = intent.getBooleanExtra(EXTRA_SKIP_MAPS, false); resetMode = MODE_RESET.equals(intent.getStringExtra(EXTRA_MODE)); startForeground(NOTIFICATION_ID, buildNotification(getString(R.string.install_busy_provisioning))); @@ -386,8 +389,9 @@ public void onError(String error) { private void runMapsAnsible() { if (cancelled) return; - if (tier == InstallationPlanner.Tier.BASIC) { - // BASIC bypass: maps already provisioned in the base image. + if (skipMaps || tier == InstallationPlanner.Tier.BASIC) { + // Maps ship in the software block (base image) and BASIC already has them; the + // content flow disables the maps reinstall. Skip straight to success. postProvisioning(getString(R.string.install_status_maps_provisioned)); new Handler(Looper.getMainLooper()).postDelayed(this::finishSuccess, 1500); return; diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java index db7d413d..0a6f8bd4 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -97,7 +97,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c wikiSkip.setOnClickListener(x -> setWiki(!wikiInc)); booksCheck.setOnClickListener(x -> setBooks(booksCheck.isChecked())); booksSkip.setOnClickListener(x -> setBooks(!booksInc)); - mapsCheck.setOnClickListener(x -> mapsCheck.setChecked(true)); // fixed + // Maps ships in the software block — shown for reference, not installed here. + mapsCheck.setChecked(true); mapsCheck.setEnabled(false); + ((TextView) v.findViewById(R.id.k2go_maps_size)).setText("Included"); download.setOnClickListener(x -> startDownload()); v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> backOrExit()); @@ -159,7 +161,7 @@ private void refresh() { : sel.isEmpty() ? "—" : WikiVariants.gb(wikiSizeGb())); booksSize.setText("0 GB"); - double picks = (wikiInc && !sel.isEmpty() ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0) + (mapsInc ? MAPS_GB : 0); + double picks = (wikiInc && !sel.isEmpty() ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0); // Maps ships with the system applyBar(osGb, picks); download.setText(String.format(Locale.US, "Download library · %.1f GB", osGb + picks)); boolean ok = !wikiInvalid(); @@ -188,7 +190,7 @@ private void backOrExit() { private void startDownload() { if (wikiInvalid()) { Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); return; } String variant = (wikiInc && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; - boolean companion = variant != null || booksInc || mapsInc; + boolean companion = variant != null || booksInc; Intent i = new Intent(requireContext(), InstallService.class); i.setAction(InstallService.ACTION_START); i.putExtra(InstallService.EXTRA_TIER, tier().name()); @@ -197,6 +199,7 @@ private void startDownload() { i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); i.putExtra(InstallService.EXTRA_REINSTALL, false); + i.putExtra(InstallService.EXTRA_SKIP_MAPS, true); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); startActivity(new Intent(requireContext(), LibraryActivity.class) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java index 30f125b4..b15a7f01 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -174,7 +174,7 @@ private void refreshProjection() { public void onCalculated(InstallationPlanner.StorageProjection p) { if (!isAdded()) return; pOs = p.osSize; pMaps = p.mapsSize; pKiwix = p.kiwixSize; - double picks = ((wikiIncluded && variant != null) ? pKiwix : 0) + pMaps; + double picks = ((wikiIncluded && variant != null) ? pKiwix : 0); // Maps ships with the system lastTotal = pOs + picks; updateGauge(pOs, picks); updateNextLabel(); @@ -210,6 +210,7 @@ private void startDownload() { i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); i.putExtra(InstallService.EXTRA_REINSTALL, false); + i.putExtra(InstallService.EXTRA_SKIP_MAPS, true); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); else requireContext().startService(i); startActivity(new Intent(requireContext(), LibraryActivity.class)