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);
+ }
}
}
@@ -223,10 +278,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");
@@ -234,19 +289,29 @@ 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 && 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
+ // "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) {
@@ -280,6 +345,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(Context context, Tier tier) {
RootfsCatalog catalog = new RootfsCatalog(context);
RootfsRepository repository =
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/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/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/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/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/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);
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..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)));
@@ -194,8 +197,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 +228,7 @@ private void runPipeline() {
}
}
if (cancelled) return;
+ persistInstalledTier();
startRootfsDownload();
} catch (Exception e) {
Log.e(TAG, "Install pipeline crashed", e);
@@ -307,7 +328,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() {
@@ -368,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/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);
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/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
new file mode 100644
index 00000000..c0ecf5f9
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java
@@ -0,0 +1,310 @@
+package org.iiab.controller.redesign;
+
+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;
+import android.util.Log;
+import android.view.View;
+import androidx.appcompat.app.AppCompatActivity;
+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;
+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:
+ * 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 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;
+ /** 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;
+
+ @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, WizardActivity.class));
+ finish();
+ return;
+ }
+
+ 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);
+ }
+
+ 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.
+ 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");
+ bootGate.setRepeatCount(LottieDrawable.INFINITE);
+ 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();
+
+ ServerStateRepository.get().state().observe(this, s -> {
+ if (s == null) return;
+ if (closing) {
+ if (!s.alive) onClosedReady();
+ } else if (s.alive) {
+ onServerReady();
+ }
+ });
+
+ // 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 (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 (!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() {
+ if (gateDismissed || bootGate == null) {
+ return;
+ }
+ gateDismissed = true;
+ hideInstallProgress();
+ if (reduceMotion()) { bootGate.setVisibility(View.GONE); return; }
+ 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) {
+ 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";
+ }
+ 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 {
+ 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();
+ if (serverController != null) serverController.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.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 && !reduceMotion()) {
+ 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; }
+ if (reduceMotion()) { 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() { }
+ @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/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..d4698f92
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java
@@ -0,0 +1,194 @@
+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, 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; int amberStreak = 0;
+ 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 -> {
+ // 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;
+ }
+
+ 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 {
+ 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();
+ }
+ }
+
+ @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()) return;
+ if (st == GREEN || st == GRAY) { c.amberStreak = 0; applyState(c, st); }
+ else { c.amberStreak++; applyState(c, c.amberStreak >= 4 ? RED : AMBER); }
+ });
+ });
+ }
+ }
+
+ 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;
+ 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);
+ 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/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/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..a0b8ec5b
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java
@@ -0,0 +1,127 @@
+package org.iiab.controller.redesign;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+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 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;
+import org.iiab.controller.feedback.presentation.FeedbackFragment;
+
+/** 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 {
+
+ @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);
+ 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;
+ }
+
+ private void openSub(String screen) {
+ if (getActivity() instanceof LibraryActivity) {
+ ((LibraryActivity) getActivity()).openSettingsSub(SettingsSubFragment.newInstance(screen));
+ }
+ }
+
+ private void openFeedback() {
+ if (getActivity() instanceof LibraryActivity) {
+ ((LibraryActivity) getActivity()).openSettingsSub(new FeedbackFragment());
+ }
+ }
+
+ 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 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(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(ctx);
+ off.setText("Turn off K2Go");
+ off.setGravity(Gravity.CENTER);
+ 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_turnoff_bg);
+ off.setClickable(true);
+ off.setOnClickListener(v -> confirmTurnOff());
+ 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. (Home or back only minimize it — it keeps running in the background.)")
+ .setNegativeButton("Cancel", null)
+ .setPositiveButton("Turn off", (d, w) -> {
+ if (getActivity() instanceof LibraryActivity) {
+ ((LibraryActivity) getActivity()).turnOffK2Go();
+ }
+ })
+ .show();
+ }
+
+ private SharedPreferences prefs() {
+ return requireContext().getSharedPreferences(
+ getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
+ }
+
+ private String versionName() {
+ try {
+ 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
new file mode 100644
index 00000000..74798efd
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java
@@ -0,0 +1,129 @@
+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 */ }
+ }
+
+ /** 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);
+ i.putExtra(org.iiab.controller.MainActivity.EXTRA_TERMINAL_ONLY, 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.");
+ 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.row(ctx, list, "Terminal (Debian)", null, null, v -> openTerminal(ctx));
+ }
+
+ private String versionName(Context ctx) {
+ try {
+ return 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..402909ed
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java
@@ -0,0 +1,125 @@
+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);
+ // 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, new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT, dp(c, 28)));
+ }
+
+ 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/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..19bf52f4
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java
@@ -0,0 +1,93 @@
+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 (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;
+
+ /** 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
+ // 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) {
+ 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, first)
+ .commit();
+ }
+ }
+
+ public void setSelectedTier(InstallationPlanner.Tier tier) { this.selectedTier = tier; }
+ public InstallationPlanner.Tier getSelectedTier() { return selectedTier; }
+
+ public boolean isEverything() { return contentEverything; }
+ public void setEverything(boolean b) { contentEverything = b; }
+ 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 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();
+ }
+
+ /** Called by Step 1 "Next". */
+ public void goToStep2() {
+ getSupportFragmentManager().beginTransaction()
+ .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/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java
new file mode 100644
index 00000000..b81dd6c7
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java
@@ -0,0 +1,165 @@
+package org.iiab.controller.redesign;
+
+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.
+ * 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 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;
+ }
+ }
+
+ 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",
+ "Wikipedia reader app + Books app", false));
+ editions.add(new Edition(InstallationPlanner.Tier.STANDARD, "Standard",
+ "Basic plus Courses app", true));
+ editions.add(new Edition(InstallationPlanner.Tier.FULL, "Full",
+ "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);
+ 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 -> {
+ if (getActivity() instanceof SetupLibraryActivity) {
+ ((SetupLibraryActivity) getActivity()).setSelectedTier(selected);
+ ((SetupLibraryActivity) getActivity()).goToStep2();
+ }
+ });
+
+ 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();
+ startActivity(new android.content.Intent(requireContext(), LibraryActivity.class));
+ requireActivity().finish();
+ });
+ select(selected);
+ 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) {
+ ((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) {
+ InstallationPlanner.calculateProjectedSize(requireContext(), tier, false,
+ ContentLanguage.systemDefault(), null,
+ 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 */ }
+ });
+ }
+
+ 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.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
new file mode 100644
index 00000000..0a6f8bd4
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java
@@ -0,0 +1,212 @@
+package org.iiab.controller.redesign;
+
+import android.content.Intent;
+import android.os.Bundle;
+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;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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;
+import org.iiab.controller.applang.data.ContentLanguage;
+import org.iiab.controller.install.presentation.InstallService;
+import org.json.JSONObject;
+
+/**
+ * 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;
+
+ 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, wikiHint;
+ private ViewGroup versions;
+ 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 double wikiSizeGb() {
+ String p = WikiVariants.primary(sel);
+ double s = (p == null) ? -1 : WikiVariants.sizeGb(langData, p);
+ return s >= 0 ? s : 0;
+ }
+
+ @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();
+ 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);
+ bP = v.findViewById(R.id.k2go_bar_picks); bF = v.findViewById(R.id.k2go_bar_free);
+ legend = v.findViewById(R.id.k2go_legend);
+ download = v.findViewById(R.id.k2go_download);
+ 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);
+ 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));
+ 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));
+ // 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());
+
+ 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) {
+ 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) { }
+ });
+
+ setWiki(wikiInc);
+ return v;
+ }
+
+ private void toggle(View body, TextView chevron) {
+ boolean show = body.getVisibility() != View.VISIBLE;
+ body.setVisibility(show ? View.VISIBLE : View.GONE);
+ chevron.setText(show ? "▾" : "▸");
+ }
+
+ 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);
+ // 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();
+ }
+ 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 boolean wikiInvalid() { return wikiInc && sel.isEmpty(); }
+
+ private void refresh() {
+ 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 && !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();
+ download.setEnabled(ok);
+ download.setAlpha(ok ? 1f : 0.5f);
+ }
+
+ 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 · 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) ? WikiVariants.primary(sel) : "";
+ boolean companion = variant != null || booksInc;
+ 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, 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)
+ .putExtra(LibraryActivity.EXTRA_INSTALLING, true));
+ requireActivity().finish();
+ }
+
+ 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/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..b15a7f01
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java
@@ -0,0 +1,220 @@
+package org.iiab.controller.redesign;
+
+import android.content.Intent;
+import android.os.Bundle;
+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;
+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 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). 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 {
+
+ private static final String[] NAMES = {"Wikipedia", "Books", "Maps", "Courses", "Review"};
+ private static final String[] INFO = {
+ "", "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;
+ private String lang;
+ 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, wikiHint;
+ private CheckBox wikiCheck;
+ private MultiResourceGaugeView gauge;
+ 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; }
+
+ @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();
+ 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]);
+ caption = v.findViewById(R.id.k2go_step_caption);
+ 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);
+ btnSkip = v.findViewById(R.id.k2go_btn_skip);
+ wikiCheck = v.findViewById(R.id.k2go_wiki_check);
+ wikiBlock = v.findViewById(R.id.k2go_wiki_block);
+ 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();
+ persist();
+ versions.setVisibility(wikiIncluded ? View.VISIBLE : View.GONE);
+ updateHint();
+ refreshProjection();
+ });
+ btnBack.setOnClickListener(x -> {
+ 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) {
+ wikiIncluded = false;
+ wikiCheck.setChecked(false);
+ versions.setVisibility(View.GONE);
+ persist();
+ updateHint();
+ refreshProjection();
+ }
+ if (step < 4) { step++; render(); }
+ });
+ 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().setWikiIncluded(wikiIncluded); act().setWikiView(picker.getMode()); }
+ }
+
+ 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);
+ 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);
+ updateHint();
+ updateNextLabel();
+ }
+
+ 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");
+ }
+
+ private void refreshProjection() {
+ String variant = (wikiIncluded && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : "";
+ 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 && variant != null) ? pKiwix : 0); // Maps ships with the system
+ lastTotal = pOs + picks;
+ updateGauge(pOs, picks);
+ updateNextLabel();
+ }
+ @Override public void onError(String e) { }
+ });
+ }
+
+ private void updateGauge(double systemGb, double picksGb) {
+ double total = StorageInfo.totalGb();
+ double used = StorageInfo.usedGb();
+ 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));
+ }
+
+ private void startDownload() {
+ 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());
+ 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, 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)
+ .putExtra(LibraryActivity.EXTRA_INSTALLING, true));
+ requireActivity().finish();
+ }
+}
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/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/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..c449b614
--- /dev/null
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java
@@ -0,0 +1,235 @@
+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, back;
+ 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);
+ 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);
+
+ // 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();
+ });
+ 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());
+ 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 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);
+ 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);
+ back.setVisibility(step >= 1 ? View.VISIBLE : View.GONE);
+
+ switch (step) {
+ case 0:
+ 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/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_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/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/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_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_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_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/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/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/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 @@
+
+
+
+
+
+
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/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml
new file mode 100644
index 00000000..91d4cc5c
--- /dev/null
+++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..bbe1086d
--- /dev/null
+++ b/controller/app/src/main/res/layout/activity_library.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..597d9ae7
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..4311a12b
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_connect.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..e1a73c4c
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_library.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/layout/fragment_k2go_settings.xml b/controller/app/src/main/res/layout/fragment_k2go_settings.xml
new file mode 100644
index 00000000..ad548b3e
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_settings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
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..e29f0ad1
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..93cf3aa1
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 00000000..abc28d43
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..76d9f4c8
--- /dev/null
+++ b/controller/app/src/main/res/layout/view_k2go_card.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..7bd48ad1
--- /dev/null
+++ b/controller/app/src/main/res/layout/view_k2go_edition.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/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
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..72509441
--- /dev/null
+++ b/controller/app/src/main/res/values/strings_k2go.xml
@@ -0,0 +1,101 @@
+
+
+
+ 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
+ 1 ✓ System → 2 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.
+ 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
+ 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.
+ 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
+ Preview — not functional yet
+
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 @@
+
+
+
+
+
+
+
+
+
+
+