From d5bb48565ba52270883a65742f9f331c655d3447 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 19:09:46 -0600 Subject: [PATCH 01/12] ADFA-5169 docs(pending): design for seeing and cancelling queued content orders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State-spine finding 6. A banked ZIM/Books/Courses order is invisible when nothing runs and can't be cancelled. Design: a Settings entry to a minimal "Pending downloads" screen that lists each queued order by item with a manual Cancel — never auto-cancel; auto-purge stays limited to system-gone. Reuses ContentType/PendingContent; adds ZimWishlist.remove. Layered feature package org.iiab.controller.pending. Seed of the future task registry (index vivo / registro), not the registry itself. --- .../ADFA-5169-pending-downloads-design.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 controller/docs/ADFA-5169-pending-downloads-design.md diff --git a/controller/docs/ADFA-5169-pending-downloads-design.md b/controller/docs/ADFA-5169-pending-downloads-design.md new file mode 100644 index 00000000..fe669f22 --- /dev/null +++ b/controller/docs/ADFA-5169-pending-downloads-design.md @@ -0,0 +1,135 @@ +# ADFA-5169 — Pending downloads: see and cancel queued content orders + +State-spine finding 6 (DEAD CONTROL). Design doc. 2026-08-17. + +## Problem + +A content order (ZIM, Books, Courses) is *banked* when it has been placed but not +yet drained — the drain is deferred while the system is not ready to run it. In that +state two things fail today: + +- It is invisible. The Home header shows "See progress" only while something is + actually running (`PendingContent.anyRunning()`, sessions only). The per-card + action sheet's `isScheduled()` reads only the maps and module wishlists, never the + three content ones. +- It cannot be cancelled. The only cancel in the app is the module sheet's, and it + clears only the maps and module wishlists. + +So a queued ZIM/Books/Courses order sits with no way to see it and no way to back +out of it. + +## What a banked order actually is + +All banked orders are legitimate user requests. The real axis is not "banked vs +legitimate" but "can the current system still drain it?": + +- Healthy system: the Home pump drains on the next poll (~3s), so the order is + transient — or it waits legitimately behind another live download (streams + serialize). This waiting window is where visibility and manual cancel earn their + keep. +- No / damaged system: the order can't drain; the real problem is the system + (recover / install), the order is secondary. +- System replaced or lost: the order is orphaned and is already purged in bulk by + `PendingWork.clearAll` on setup / replacement / invalidation. + +Consequence for this design: never auto-cancel a legitimate order, and never purge +by age. Cancel is a manual "I changed my mind." Auto-purge stays limited to the +system-gone case, which already exists. + +## Scope (decided in brainstorming) + +In scope: + +- A **Settings row**, always visible ("Pending downloads"), showing a count when + there is something queued and "No pending downloads" when empty. It never pushes; + the user goes when they want. +- A minimal **Pending downloads** screen that lists each queued order **by item** + (this Wikipedia collection, this book, this course channel) with its size and its + own Cancel. Manual only. +- Cancel removes that one order; the rest stay. +- If a download is actually running, a link to the live index + (`SetupProgressActivity`). The pending screen itself stays about what is queued. +- Only the three live content types: **ZIM, Books, Courses**. + +Out of scope (explicit): + +- Maps and module orders — they already have a cancel path; leaving them avoids a + second place that cancels the same thing. +- Controls for *running* tasks (retry / pause / resume / cancel) — that is the + download-contract family (ADFA-4894 / 4896 / 4899, and the surface built with + ADFA-5119). +- The durable task registry and history — the larger, deliberately deferred design + this is a first slice of. +- Any change to auto-purge. + +## North star (not built here) + +The eventual model is two surfaces: an **index** = the live, self-cleaning view of +what is happening now (never shows anything stuck); and a **registry** = the durable +record of everything the index ever showed, plus the pending orders that need a +decision. This screen is the seed of the registry's "pending decision" bucket. The +invariant that keeps the two from blurring: nothing stuck ever lives in the index; +the moment a task can't proceed it belongs to the pending surface. Building the full +registry (a first-class Task entity, durable across process death — overlapping +ADFA-4897 / 4874) is a separate effort. + +## Design (layered, its own feature package) + +`org.iiab.controller.pending`, following the reference slice pattern. + +### domain/ (pure JVM, no Android) + +- `PendingOrder` — an entity: `ContentType type`, `String id`, `String name`, + `long bytes`. One queued order. +- `PendingOrdersRepository` — port: `List list()`, + `void cancel(PendingOrder order)`. + +### data/ + +- `PendingOrdersRepositoryImpl` — reads the three content wishlists (ZIM, Books, + Courses) into `PendingOrder`s and cancels via per-item removal. It is the single + place that maps a type to its wishlist for listing and cancel, sitting alongside + the knowledge `PendingContent` / `ContentType` already own — no second copy. +- One new wishlist method: `ZimWishlist.remove(id)`. Books (`remove(id)`) and Kolibri + (`remove(channelId)`) already have per-item removal; ZIM has only `clear` today. + +### presentation/ + +- `PendingOrdersViewModel` + `PendingOrdersUiState` + `PendingOrdersViewModelFactory`, + wired by hand (no DI). +- The screen (`PendingOrdersFragment` or Activity), Material 3: semantic colour + tokens, type scale, 4dp grid, `MaterialAlertDialog` / snackbar via + `SnackbarDuration`. It observes the ViewModel; it does not read or format the + wishlists itself. +- The Settings row — an additive edit to `SettingsFragment` / `SettingsUi`, the one + touch to a shared hotspot, kept minimal. The link to the live index reuses + `SetupProgressActivity`. + +### Data flow + +Open → ViewModel loads `list()` from the wishlists → render, or the empty state → +Cancel on a row → `cancel(order)` removes that item → reload → the row disappears, +with a snackbar. No automatic action anywhere. + +## Testing + +- Domain: `PendingOrder` and any pure rule (ordering / grouping) get JVM unit tests. +- Data: the wishlist read / per-item cancel is Android (SharedPreferences), verified + on device. +- New user-facing strings ship in all 33 locales in the same PR (a missing + translation fails the build). + +## Seams / reuse + +- Reuse `ContentType` + `PendingContent`; no duplicated wishlist knowledge. +- One new low-level method: `ZimWishlist.remove(id)`. +- Settings row is additive. +- The live-index link reuses `SetupProgressActivity`. + +## Acceptance + +- A queued content order is visible when nothing is running. +- A queued ZIM, Books or Courses order can be cancelled from the UI, and cancelling + leaves the rest untouched and removes nothing it should not. +- Maps and module orders keep their current behavior. +- Nothing is cancelled or purged automatically by this feature. From 51d12e66c5a2beb5ec78969f6fd771bbbc16f935 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 19:17:15 -0600 Subject: [PATCH 02/12] =?UTF-8?q?ADFA-5169=20feat(pending):=20domain=20?= =?UTF-8?q?=E2=80=94=20PendingOrder=20+=20repository=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pure-JVM core of the pending-orders feature (finding 6): PendingOrder (type/id/name/bytes) with a stable DISPLAY_ORDER (group by content type, then name, then id), and the PendingOrdersRepository port (list/cancel) the data layer will implement. JVM unit test for the ordering. No Android, no wiring yet. --- .../pending/domain/PendingOrder.java | 66 +++++++++++++++++++ .../domain/PendingOrdersRepository.java | 29 ++++++++ .../pending/domain/PendingOrderTest.java | 59 +++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrder.java create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrdersRepository.java create mode 100644 controller/app/src/test/java/org/iiab/controller/pending/domain/PendingOrderTest.java diff --git a/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrder.java b/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrder.java new file mode 100644 index 00000000..74fd56c9 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrder.java @@ -0,0 +1,66 @@ +/* + * ============================================================================ + * Name : PendingOrder.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : One queued (banked) content order — a single ZIM collection, + * book, or course channel the user asked for that has not been + * drained yet. Pure JVM domain entity (ADFA-5169, finding 6). + * ============================================================================ + */ +package org.iiab.controller.pending.domain; + +import org.iiab.controller.system.domain.ContentType; + +import java.util.Comparator; + +/** + * A single queued content order. + * + *

Immutable value object. {@code id} is the wishlist key used to cancel this one + * order (a ZIM file id, a book id, a Kolibri channel id); {@code name} is what the + * user reads; {@code bytes} is its size, {@code <= 0} when unknown. + * + *

Pure domain type: no Android. What each type is and how it runs lives in + * {@link ContentType}; how the orders are stored lives in the data layer. + */ +public final class PendingOrder { + + private final ContentType type; + private final String id; + private final String name; + private final long bytes; + + public PendingOrder(ContentType type, String id, String name, long bytes) { + this.type = type; + this.id = id; + this.name = name; + this.bytes = bytes; + } + + public ContentType type() { + return type; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public long bytes() { + return bytes; + } + + /** + * Stable display order for the pending list: grouped by content type (the enum's + * own order — ZIM, Books, Courses), then by name (case-insensitive), then by id + * so ties never reorder between reads. Null names sort as empty and never throw. + */ + public static final Comparator DISPLAY_ORDER = + Comparator.comparingInt((PendingOrder o) -> o.type == null ? Integer.MAX_VALUE : o.type.ordinal()) + .thenComparing(o -> o.name == null ? "" : o.name, String.CASE_INSENSITIVE_ORDER) + .thenComparing(o -> o.id == null ? "" : o.id); +} diff --git a/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrdersRepository.java b/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrdersRepository.java new file mode 100644 index 00000000..7b9a3700 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/domain/PendingOrdersRepository.java @@ -0,0 +1,29 @@ +/* + * ============================================================================ + * Name : PendingOrdersRepository.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Domain port for listing and cancelling queued content orders. + * The data layer maps each type to its wishlist (ADFA-5169). + * ============================================================================ + */ +package org.iiab.controller.pending.domain; + +import java.util.List; + +/** + * The abstraction the domain owns for queued content orders; the data layer provides + * the implementation. The domain never learns where the orders are stored. + * + *

Implementations must never throw: {@link #list()} returns an empty list when + * nothing is queued (or a wishlist cannot be read), and {@link #cancel} is a no-op + * when the order is already gone. + */ +public interface PendingOrdersRepository { + + /** Every queued content order across the live content types, or empty. */ + List list(); + + /** Removes one queued order from its wishlist. The rest are untouched. */ + void cancel(PendingOrder order); +} diff --git a/controller/app/src/test/java/org/iiab/controller/pending/domain/PendingOrderTest.java b/controller/app/src/test/java/org/iiab/controller/pending/domain/PendingOrderTest.java new file mode 100644 index 00000000..3d98a18a --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/pending/domain/PendingOrderTest.java @@ -0,0 +1,59 @@ +/* + * ============================================================================ + * Name : PendingOrderTest.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : JVM unit tests for PendingOrder.DISPLAY_ORDER (ADFA-5169). + * ============================================================================ + */ +package org.iiab.controller.pending.domain; + +import static org.junit.Assert.assertEquals; + +import org.iiab.controller.system.domain.ContentType; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PendingOrderTest { + + @Test + public void displayOrderGroupsByTypeThenName() { + PendingOrder zimEs = new PendingOrder(ContentType.ZIM, "z2", "Wikipedia (Spanish)", 3); + PendingOrder zimEn = new PendingOrder(ContentType.ZIM, "z1", "Wikipedia (English)", 4); + PendingOrder book = new PendingOrder(ContentType.BOOKS, "b1", "Gutenberg", 1); + PendingOrder course = new PendingOrder(ContentType.COURSES, "c1", "Khan Academy", 12); + + List orders = new ArrayList<>(Arrays.asList(course, book, zimEs, zimEn)); + orders.sort(PendingOrder.DISPLAY_ORDER); + + // ZIM group first (English before Spanish), then Books, then Courses. + assertEquals(Arrays.asList("z1", "z2", "b1", "c1"), + Arrays.asList(orders.get(0).id(), orders.get(1).id(), + orders.get(2).id(), orders.get(3).id())); + } + + @Test + public void nameIsCaseInsensitiveWithinAType() { + PendingOrder lower = new PendingOrder(ContentType.BOOKS, "b1", "atlas", 0); + PendingOrder upper = new PendingOrder(ContentType.BOOKS, "b2", "Beowulf", 0); + + List orders = new ArrayList<>(Arrays.asList(upper, lower)); + orders.sort(PendingOrder.DISPLAY_ORDER); + + assertEquals("b1", orders.get(0).id()); // "atlas" before "Beowulf" regardless of case + } + + @Test + public void nullNameSortsAsEmptyAndDoesNotThrow() { + PendingOrder noName = new PendingOrder(ContentType.ZIM, "z1", null, 0); + PendingOrder named = new PendingOrder(ContentType.ZIM, "z2", "B", 0); + + List orders = new ArrayList<>(Arrays.asList(named, noName)); + orders.sort(PendingOrder.DISPLAY_ORDER); // must not throw + + assertEquals("z1", orders.get(0).id()); // empty name sorts first + } +} From db867ecacca03056cb2906f626c3c10396003ea8 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 19:35:35 -0600 Subject: [PATCH 03/12] =?UTF-8?q?ADFA-5169=20feat(pending):=20data=20?= =?UTF-8?q?=E2=80=94=20repository=20over=20the=20three=20content=20wishlis?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PendingOrdersRepositoryImpl reads the ZIM/Books/Courses wishlists into PendingOrders (sorted by DISPLAY_ORDER) and cancels one by removing it from its wishlist. Adds ZimWishlist.remove(key) — Books and Kolibri already had per-item removal. Maps and modules stay out of scope. Android/SharedPreferences; verified by compile. --- .../data/PendingOrdersRepositoryImpl.java | 99 +++++++++++++++++++ .../iiab/controller/redesign/ZimWishlist.java | 13 +++ 2 files changed, 112 insertions(+) create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImpl.java diff --git a/controller/app/src/main/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImpl.java b/controller/app/src/main/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImpl.java new file mode 100644 index 00000000..ca7e3fce --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImpl.java @@ -0,0 +1,99 @@ +/* + * ============================================================================ + * Name : PendingOrdersRepositoryImpl.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Reads the three live content wishlists (ZIM, Books, Courses) into + * PendingOrders and cancels one by removing it from its wishlist. + * The single place that maps a content type to its wishlist for the + * pending list, alongside PendingContent's existing knowledge + * (ADFA-5169). Maps and modules are out of scope by design. + * ============================================================================ + */ +package org.iiab.controller.pending.data; + +import android.content.Context; + +import org.iiab.controller.kolibri.data.KolibriWishlist; +import org.iiab.controller.pending.domain.PendingOrder; +import org.iiab.controller.pending.domain.PendingOrdersRepository; +import org.iiab.controller.redesign.BooksWishlist; +import org.iiab.controller.redesign.ZimWishlist; +import org.iiab.controller.system.domain.ContentType; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PendingOrdersRepositoryImpl implements PendingOrdersRepository { + + private final Context app; + + public PendingOrdersRepositoryImpl(Context ctx) { + this.app = ctx.getApplicationContext(); + } + + @Override + public List list() { + List out = new ArrayList<>(); + addZim(out); + addBooks(out); + addCourses(out); + Collections.sort(out, PendingOrder.DISPLAY_ORDER); + return out; + } + + @Override + public void cancel(PendingOrder order) { + if (order == null || order.type() == null || order.id() == null) { + return; + } + switch (order.type()) { + case ZIM: ZimWishlist.remove(app, order.id()); break; + case BOOKS: BooksWishlist.remove(app, order.id()); break; + case COURSES: KolibriWishlist.remove(app, order.id()); break; + default: break; // MAPS is out of scope: it keeps its own cancel path. + } + } + + /** ZIM order = {@code {key, bytes}}; the key ("project|lang|flavour") is both id + * and, absent a friendlier catalog name here, the display name. */ + private void addZim(List out) { + JSONArray a = ZimWishlist.all(app); + for (int i = 0; i < a.length(); i++) { + JSONObject o = a.optJSONObject(i); + if (o == null) continue; + String key = o.optString("key", ""); + if (key.isEmpty()) continue; + out.add(new PendingOrder(ContentType.ZIM, key, key, o.optLong("bytes", 0L))); + } + } + + /** Books order = {@code {id, title, url}}; no size is stored, so bytes is 0 (unknown). */ + private void addBooks(List out) { + JSONArray a = BooksWishlist.all(app); + for (int i = 0; i < a.length(); i++) { + JSONObject o = a.optJSONObject(i); + if (o == null) continue; + String id = o.optString("id", ""); + if (id.isEmpty()) continue; + String title = o.optString("title", ""); + out.add(new PendingOrder(ContentType.BOOKS, id, title.isEmpty() ? id : title, 0L)); + } + } + + /** Courses order = {@code {channelId, version, name, bytes}}. */ + private void addCourses(List out) { + JSONArray a = KolibriWishlist.all(app); + for (int i = 0; i < a.length(); i++) { + JSONObject o = a.optJSONObject(i); + if (o == null) continue; + String id = o.optString("channelId", ""); + if (id.isEmpty()) continue; + String name = o.optString("name", ""); + out.add(new PendingOrder(ContentType.COURSES, id, name.isEmpty() ? id : name, o.optLong("bytes", 0L))); + } + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/ZimWishlist.java b/controller/app/src/main/java/org/iiab/controller/redesign/ZimWishlist.java index 8ece64d2..bdb795f4 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/ZimWishlist.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/ZimWishlist.java @@ -63,6 +63,19 @@ public static void add(Context ctx, Map cart) { prefs(ctx).edit().putString(KEY, out.toString()).apply(); } + /** ADFA-5169: drop one order by its "project|lang|flavour" key; the rest stay. + * No-op if the key is absent. Mirrors BooksWishlist.remove / KolibriWishlist.remove. */ + public static void remove(Context ctx, String key) { + if (key == null) return; + JSONArray cur = all(ctx); + JSONArray out = new JSONArray(); + for (int i = 0; i < cur.length(); i++) { + JSONObject o = cur.optJSONObject(i); + if (o != null && !key.equals(o.optString("key"))) out.put(o); + } + prefs(ctx).edit().putString(KEY, out.toString()).apply(); + } + public static void clear(Context ctx) { prefs(ctx).edit().remove(KEY).apply(); } From 53e0e650296db4a01497b8317b652268e29626a8 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 19:46:52 -0600 Subject: [PATCH 04/12] =?UTF-8?q?ADFA-5169=20feat(pending):=20presentation?= =?UTF-8?q?=20=E2=80=94=20the=20Pending=20downloads=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PendingOrdersFragment lists each queued order by item with its own Cancel, an empty state, and a link to the live index when a download is running. It observes PendingOrdersViewModel (+ UiState + hand-wired Factory) and reuses the settings sub-screen chrome. Not reachable yet — the Settings entry lands in the next slice. Four base strings added; their 33-locale translations come with the string slice, so a lint MissingTranslation warning is expected until then. --- .../presentation/PendingOrdersFragment.java | 156 ++++++++++++++++++ .../presentation/PendingOrdersUiState.java | 42 +++++ .../presentation/PendingOrdersViewModel.java | 69 ++++++++ .../PendingOrdersViewModelFactory.java | 39 +++++ .../app/src/main/res/values/strings_k2go.xml | 4 + 5 files changed, 310 insertions(+) create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersUiState.java create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModel.java create mode 100644 controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModelFactory.java diff --git a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java new file mode 100644 index 00000000..8bcbb3cb --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java @@ -0,0 +1,156 @@ +/* + * ============================================================================ + * Name : PendingOrdersFragment.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : The "Pending downloads" screen (ADFA-5169, finding 6). Lists each + * queued content order (ZIM / Books / Courses) by item with its own + * Cancel; shows an empty state; and, when a download is running, a + * link to the live index. Observes PendingOrdersViewModel — it does + * not read or format the wishlists itself. Reuses the settings + * sub-screen chrome (title + back + scroll list). + * ============================================================================ + */ +package org.iiab.controller.pending.presentation; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +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 androidx.lifecycle.ViewModelProvider; + +import org.iiab.controller.R; +import org.iiab.controller.pending.domain.PendingOrder; +import org.iiab.controller.redesign.SetupProgressActivity; +import org.iiab.controller.util.ByteFormatter; + +public class PendingOrdersFragment extends Fragment { + + private PendingOrdersViewModel vm; + private LinearLayout list; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_settings_sub, c, false); + ((TextView) root.findViewById(R.id.k2go_sub_title)).setText(getString(R.string.k2go_pending_title)); + root.findViewById(R.id.k2go_sub_back).setOnClickListener(v -> + requireActivity().getSupportFragmentManager().popBackStack()); + list = root.findViewById(R.id.k2go_sub_list); + + vm = new ViewModelProvider(this, new PendingOrdersViewModelFactory(requireContext())) + .get(PendingOrdersViewModel.class); + vm.state().observe(getViewLifecycleOwner(), this::render); + return root; + } + + @Override + public void onResume() { + super.onResume(); + if (vm != null) vm.refresh(); // a cancel elsewhere, or a drain, may have changed the list + } + + private void render(PendingOrdersUiState st) { + if (list == null || st == null || st.loading) return; + Context ctx = requireContext(); + list.removeAllViews(); + + if (st.somethingRunning) { + list.addView(runningBanner(ctx)); + } + if (st.isEmpty()) { + list.addView(emptyText(ctx)); + return; + } + for (PendingOrder order : st.orders) { + list.addView(orderRow(ctx, order)); + } + } + + private TextView emptyText(Context ctx) { + TextView t = new TextView(ctx); + t.setText(getString(R.string.k2go_pending_empty)); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); + t.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + t.setGravity(Gravity.CENTER_HORIZONTAL); + int pad = dp(ctx, 24); + t.setPadding(pad, pad, pad, pad); + return t; + } + + private View runningBanner(Context ctx) { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + int padH = dp(ctx, 16), padV = dp(ctx, 12); + row.setPadding(padH, padV, padH, padV); + + TextView label = new TextView(ctx); + label.setText(getString(R.string.k2go_pending_running)); + label.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); + label.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + label.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(label); + + TextView link = new TextView(ctx); + link.setText(getString(R.string.k2go_pending_see_progress)); + link.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); + link.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + link.setOnClickListener(v -> startActivity(new Intent(ctx, SetupProgressActivity.class))); + row.addView(link); + return row; + } + + private View orderRow(Context ctx, PendingOrder order) { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + int padH = dp(ctx, 16), padV = dp(ctx, 12); + row.setPadding(padH, padV, padH, padV); + + LinearLayout textCol = new LinearLayout(ctx); + textCol.setOrientation(LinearLayout.VERTICAL); + textCol.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + TextView name = new TextView(ctx); + name.setText(order.name() == null ? order.id() : order.name()); + name.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleMedium); + name.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + textCol.addView(name); + + if (order.bytes() > 0L) { + TextView size = new TextView(ctx); + size.setText(ByteFormatter.toHuman(order.bytes())); + size.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + size.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + textCol.addView(size); + } + row.addView(textCol); + + ImageButton cancel = new ImageButton(ctx); + cancel.setImageResource(R.drawable.ic_close_24); + cancel.setBackground(null); + cancel.setColorFilter(ContextCompat.getColor(ctx, R.color.k2go_muted)); + cancel.setContentDescription(getString(R.string.k2go_cancel)); + int tap = dp(ctx, 44); + cancel.setLayoutParams(new LinearLayout.LayoutParams(tap, tap)); + cancel.setOnClickListener(v -> vm.cancel(order)); + row.addView(cancel); + return row; + } + + private static int dp(Context ctx, int v) { + return Math.round(v * ctx.getResources().getDisplayMetrics().density); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersUiState.java b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersUiState.java new file mode 100644 index 00000000..67c052af --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersUiState.java @@ -0,0 +1,42 @@ +/* + * ============================================================================ + * Name : PendingOrdersUiState.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Immutable view state for the Pending downloads screen (ADFA-5169): + * the queued orders to show, and whether a download is running now + * (so the screen can offer a link to the live index). + * ============================================================================ + */ +package org.iiab.controller.pending.presentation; + +import org.iiab.controller.pending.domain.PendingOrder; + +import java.util.Collections; +import java.util.List; + +public final class PendingOrdersUiState { + + public final boolean loading; + public final List orders; + public final boolean somethingRunning; + + private PendingOrdersUiState(boolean loading, List orders, boolean somethingRunning) { + this.loading = loading; + this.orders = orders; + this.somethingRunning = somethingRunning; + } + + public static PendingOrdersUiState loading() { + return new PendingOrdersUiState(true, Collections.emptyList(), false); + } + + public static PendingOrdersUiState loaded(List orders, boolean somethingRunning) { + return new PendingOrdersUiState(false, + orders == null ? Collections.emptyList() : orders, somethingRunning); + } + + public boolean isEmpty() { + return orders.isEmpty(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModel.java b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModel.java new file mode 100644 index 00000000..94dce5e0 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModel.java @@ -0,0 +1,69 @@ +/* + * ============================================================================ + * Name : PendingOrdersViewModel.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Presentation ViewModel for the Pending downloads screen (ADFA-5169). + * Reads the queued orders off the main thread and exposes a + * PendingOrdersUiState stream; cancel() removes one order and reloads. + * Cancel is user-driven only — nothing is cancelled automatically. + * ============================================================================ + */ +package org.iiab.controller.pending.presentation; + +import android.content.Context; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.iiab.controller.pending.domain.PendingOrder; +import org.iiab.controller.pending.domain.PendingOrdersRepository; +import org.iiab.controller.system.data.PendingContent; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class PendingOrdersViewModel extends ViewModel { + + private final PendingOrdersRepository repository; + private final Context app; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final MutableLiveData state = + new MutableLiveData<>(PendingOrdersUiState.loading()); + + public PendingOrdersViewModel(PendingOrdersRepository repository, Context appContext) { + this.repository = repository; + this.app = appContext.getApplicationContext(); + } + + public LiveData state() { + return state; + } + + /** Re-reads the queued orders (and whether a download is running now). */ + public void refresh() { + state.postValue(PendingOrdersUiState.loading()); + executor.execute(this::readAndPost); + } + + /** Removes one queued order, then reloads. No automatic cancellation anywhere. */ + public void cancel(PendingOrder order) { + executor.execute(() -> { + repository.cancel(order); + readAndPost(); + }); + } + + private void readAndPost() { + List orders = repository.list(); + boolean running = PendingContent.anyRunning(app); + state.postValue(PendingOrdersUiState.loaded(orders, running)); + } + + @Override + protected void onCleared() { + executor.shutdownNow(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModelFactory.java b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModelFactory.java new file mode 100644 index 00000000..7423be9a --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersViewModelFactory.java @@ -0,0 +1,39 @@ +/* + * ============================================================================ + * Name : PendingOrdersViewModelFactory.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Manual dependency wiring for PendingOrdersViewModel (ADFA-5169): + * composes data -> domain -> presentation by hand, no DI framework. + * ============================================================================ + */ +package org.iiab.controller.pending.presentation; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.iiab.controller.pending.data.PendingOrdersRepositoryImpl; +import org.iiab.controller.pending.domain.PendingOrdersRepository; + +public class PendingOrdersViewModelFactory implements ViewModelProvider.Factory { + + private final Context ctx; + + public PendingOrdersViewModelFactory(Context ctx) { + this.ctx = ctx.getApplicationContext(); + } + + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(PendingOrdersViewModel.class)) { + PendingOrdersRepository repository = new PendingOrdersRepositoryImpl(ctx); + return (T) new PendingOrdersViewModel(repository, ctx); + } + throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass.getName()); + } +} diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 17aee4e0..5d2a1be0 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -899,4 +899,8 @@ Done Join Open + Pending downloads + No pending downloads. + Something is downloading now + See progress From 8e1dc2b8b436bca6a7d4d3e27e7f9701974fd67b Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 19:57:23 -0600 Subject: [PATCH 05/12] ADFA-5169 feat(pending): Settings entry to the Pending downloads screen An always-visible "Pending downloads" row under Settings > System, showing the queued count or "None", opening PendingOrdersFragment as a sub-screen (bottom nav stays). Makes finding 6 reachable. Three row strings added; their translations land with the string slice. --- .../controller/redesign/SettingsFragment.java | 17 +++++++++++++++++ .../app/src/main/res/values/strings_k2go.xml | 3 +++ 2 files changed, 20 insertions(+) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index 60790bd4..c43adf02 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -43,6 +43,14 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c i.putExtra(SetupLibraryActivity.EXTRA_MODULE_MGMT, true); ctx.startActivity(i); }); + // ADFA-5169: always-visible entry to the queued content orders (finding 6). Shows a count, or + // "None" when empty. Opens as a settings sub-screen (bottom nav stays), like Language/About. + SettingsUi.row(ctx, list, getString(R.string.k2go_settings_pending), null, pendingValue(ctx), v -> { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).openSettingsSub( + new org.iiab.controller.pending.presentation.PendingOrdersFragment()); + } + }); SettingsUi.row(ctx, list, getString(R.string.k2go_settings_backups), getString(R.string.k2go_br_row_sub), null, v -> { android.content.Intent i = new android.content.Intent(ctx, SetupLibraryActivity.class); i.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true); @@ -62,6 +70,15 @@ private void openSub(String screen) { } } + /** ADFA-5169: the right-aligned value for the Pending downloads row — a count, or "None". + * A cheap read (three SharedPreferences reads via the repository), safe on the main thread. */ + private String pendingValue(Context ctx) { + int n = new org.iiab.controller.pending.data.PendingOrdersRepositoryImpl(ctx).list().size(); + return n > 0 + ? getString(R.string.k2go_settings_pending_count, n) + : getString(R.string.k2go_settings_pending_none); + } + private void openFeedback() { if (getActivity() instanceof LibraryActivity) { ((LibraryActivity) getActivity()).openSettingsSub(new FeedbackFragment()); diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 5d2a1be0..cc0ed547 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -903,4 +903,7 @@ No pending downloads. Something is downloading now See progress + Pending downloads + %d queued + None From 7e69565c6edd6232b1a46e9a878c51ea8f6a9bc8 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 20:25:52 -0600 Subject: [PATCH 06/12] ADFA-5169 test(pending): adb seed hook to validate the Pending screen (debug only) DebugSeedPendingReceiver (src/debug) seeds two ZIM, one Books and one Courses banked order from adb so the Pending downloads screen can be validated without arranging a deferred drain. Never ships in release; removed before merge. --- controller/app/src/debug/AndroidManifest.xml | 9 ++++ .../debug/DebugSeedPendingReceiver.java | 54 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java diff --git a/controller/app/src/debug/AndroidManifest.xml b/controller/app/src/debug/AndroidManifest.xml index 200c0910..d1e3d319 100644 --- a/controller/app/src/debug/AndroidManifest.xml +++ b/controller/app/src/debug/AndroidManifest.xml @@ -12,5 +12,14 @@ + + + + + + diff --git a/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java b/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java new file mode 100644 index 00000000..fa09499a --- /dev/null +++ b/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java @@ -0,0 +1,54 @@ +package org.iiab.controller.pending.debug; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; + +import org.iiab.controller.kolibri.data.KolibriWishlist; +import org.iiab.controller.redesign.BooksWishlist; +import org.iiab.controller.redesign.ZimWishlist; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * DEBUG-ONLY (lives in src/debug; never ships in release). Seeds a few banked content + * orders — two ZIM, one Books, one Courses — so the Pending downloads screen (ADFA-5169, + * finding 6) can be validated without arranging a real deferred drain. + * + *

+ * adb shell am broadcast \
+ *   -a org.iiab.controller.DEBUG_SEED_PENDING \
+ *   -n org.iiab.controller/org.iiab.controller.pending.debug.DebugSeedPendingReceiver
+ * 
+ * + * Then open Settings > Pending downloads to see them and Cancel each. Stay off the Home + * tab while validating: Home's pump drains the wishlists on a live system, which would + * clear the seed. Watch {@code adb logcat -s IIAB-DebugSeedPending}. Remove before merge. + */ +public final class DebugSeedPendingReceiver extends BroadcastReceiver { + + private static final String TAG = "IIAB-DebugSeedPending"; + + @Override + public void onReceive(Context context, Intent intent) { + Context app = context.getApplicationContext(); + + Map zim = new LinkedHashMap<>(); + zim.put("wikipedia|en|maxi", 4_200_000_000L); + zim.put("wikipedia|es|maxi", 3_100_000_000L); + ZimWishlist.add(app, zim); + + BooksWishlist.add(app, "gutenberg-fiction", "Gutenberg — Fiction", + "https://example.invalid/fiction.epub"); + + KolibriWishlist.add(app, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", 1, + "Khan Academy — Math", 12_000_000_000L, null); + + Log.i(TAG, "Seeded pending orders: 2 ZIM, 1 Books, 1 Courses. " + + "Open Settings > Pending downloads (stay off Home)."); + Toast.makeText(app, "Debug: seeded 4 pending orders", Toast.LENGTH_SHORT).show(); + } +} From 61e05c034557da4ffbeff1e8343b9e1e1ca89c90 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 20:35:01 -0600 Subject: [PATCH 07/12] ADFA-5169 feat(pending): Settings entry to the Pending downloads screen An always-visible "Pending downloads" row under Settings > System, showing the queued count or "None", opening PendingOrdersFragment as a sub-screen (bottom nav stays). The count refreshes in onResume, so it is current on tab entry and on return from the screen. Makes finding 6 reachable. Three row strings added; their translations land with the string slice. --- .../controller/redesign/SettingsFragment.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java index c43adf02..ded450a5 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -23,6 +23,9 @@ * open as sub-screens (bottom nav stays); Theme + Language + Send feedback are functional. */ public class SettingsFragment extends Fragment { + // ADFA-5169: kept so onResume can refresh its count when Settings becomes visible. + private View pendingRow; + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { @@ -45,7 +48,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); // ADFA-5169: always-visible entry to the queued content orders (finding 6). Shows a count, or // "None" when empty. Opens as a settings sub-screen (bottom nav stays), like Language/About. - SettingsUi.row(ctx, list, getString(R.string.k2go_settings_pending), null, pendingValue(ctx), v -> { + pendingRow = SettingsUi.row(ctx, list, getString(R.string.k2go_settings_pending), null, pendingValue(ctx), v -> { if (getActivity() instanceof LibraryActivity) { ((LibraryActivity) getActivity()).openSettingsSub( new org.iiab.controller.pending.presentation.PendingOrdersFragment()); @@ -79,6 +82,19 @@ private String pendingValue(Context ctx) { : getString(R.string.k2go_settings_pending_none); } + @Override + public void onResume() { + super.onResume(); + refreshPendingValue(); // keep the count current on tab entry and on return from the screen + } + + /** ADFA-5169: update the pending row's value in place — its children are [column, value, chevron]. */ + private void refreshPendingValue() { + if (!(pendingRow instanceof ViewGroup) || getContext() == null) return; + View v = ((ViewGroup) pendingRow).getChildAt(1); + if (v instanceof TextView) ((TextView) v).setText(pendingValue(requireContext())); + } + private void openFeedback() { if (getActivity() instanceof LibraryActivity) { ((LibraryActivity) getActivity()).openSettingsSub(new FeedbackFragment()); From 1b467f6d106097d2135ad66ae03203da50e69fc5 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 20:41:47 -0600 Subject: [PATCH 08/12] ADFA-5169 fix(pending): use the M3 delete icon for the per-row cancel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homologates the cancel affordance with the maps (FqrController) and Kiwix (KiwixManageController) per-row deletes: ic_delete_24 in the danger colour instead of a "✕" glyph, so removing a queued order reads as a delete. No confirm dialog — a queued order has nothing downloaded to lose yet. --- .../presentation/PendingOrdersFragment.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java index 8bcbb3cb..432f8cac 100644 --- a/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/pending/presentation/PendingOrdersFragment.java @@ -20,7 +20,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -138,12 +138,14 @@ private View orderRow(Context ctx, PendingOrder order) { } row.addView(textCol); - ImageButton cancel = new ImageButton(ctx); - cancel.setImageResource(R.drawable.ic_close_24); - cancel.setBackground(null); - cancel.setColorFilter(ContextCompat.getColor(ctx, R.color.k2go_muted)); + // ADFA-5027 homologation: the official M3 delete (trash) icon in the danger colour, as the + // maps (FqrController) and Kiwix (KiwixManageController) per-row deletes use — not a "✕" glyph. + ImageView cancel = new ImageView(ctx); + cancel.setImageResource(R.drawable.ic_delete_24); + cancel.setColorFilter(ContextCompat.getColor(ctx, R.color.k2go_clay)); cancel.setContentDescription(getString(R.string.k2go_cancel)); - int tap = dp(ctx, 44); + int pad = dp(ctx, 8), tap = dp(ctx, 44); + cancel.setPadding(pad, pad, pad, pad); cancel.setLayoutParams(new LinearLayout.LayoutParams(tap, tap)); cancel.setOnClickListener(v -> vm.cancel(order)); row.addView(cancel); From 8e896472e4153969e9561cea83aec3a75b3d2619 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 20:46:34 -0600 Subject: [PATCH 09/12] ADFA-5169 i18n(pending): translate the pending-downloads strings (33 locales) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seven new strings — the screen's title/empty/running/see-progress and the Settings row's label/count/none — in all 33 locales, so lint MissingTranslation passes. --- controller/app/src/main/res/values-ar/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-az/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-bg/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-bn/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-cs/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-de/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-el/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-es/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-fa/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-fr/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-gu/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-hi/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-hu/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-in/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-it/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-ja/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-ko/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-lt/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-nl/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-no/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-pl/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-pt/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-ro/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-ru-rRU/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-sk/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-sr/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-sw/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-ta/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-tr/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-uk/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-vi/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-yo/strings_k2go.xml | 7 +++++++ controller/app/src/main/res/values-zh-rCN/strings_k2go.xml | 7 +++++++ 33 files changed, 231 insertions(+) diff --git a/controller/app/src/main/res/values-ar/strings_k2go.xml b/controller/app/src/main/res/values-ar/strings_k2go.xml index 22484f70..1fbd9e59 100644 --- a/controller/app/src/main/res/values-ar/strings_k2go.xml +++ b/controller/app/src/main/res/values-ar/strings_k2go.xml @@ -816,4 +816,11 @@ استعادة قيد التنفيذ. يُرجى الانتظار حتى تنتهي العملية. تثبيت قيد التنفيذ. يُرجى الانتظار حتى تنتهي العملية. تنزيل قيد التنفيذ. يُرجى الانتظار حتى تنتهي العملية. + التنزيلات المعلقة + لا توجد تنزيلات معلقة. + يجري تنزيل شيء الآن + عرض التقدم + التنزيلات المعلقة + %d في قائمة الانتظار + لا شيء diff --git a/controller/app/src/main/res/values-az/strings_k2go.xml b/controller/app/src/main/res/values-az/strings_k2go.xml index 38026355..cfac39ce 100644 --- a/controller/app/src/main/res/values-az/strings_k2go.xml +++ b/controller/app/src/main/res/values-az/strings_k2go.xml @@ -816,4 +816,11 @@ Bərpa davam edir. Zəhmət olmasa, əməliyyatın bitməsini gözləyin. Quraşdırma davam edir. Zəhmət olmasa, əməliyyatın bitməsini gözləyin. Yükləmə davam edir. Zəhmət olmasa, əməliyyatın bitməsini gözləyin. + Gözləyən yükləmələr + Gözləyən yükləmə yoxdur. + İndi nəsə yüklənir + Gedişata bax + Gözləyən yükləmələr + %d növbədə + Yoxdur diff --git a/controller/app/src/main/res/values-bg/strings_k2go.xml b/controller/app/src/main/res/values-bg/strings_k2go.xml index 0e250882..2368c7b0 100644 --- a/controller/app/src/main/res/values-bg/strings_k2go.xml +++ b/controller/app/src/main/res/values-bg/strings_k2go.xml @@ -816,4 +816,11 @@ Възстановяване е в ход. Изчакайте операцията да приключи. Инсталиране е в ход. Изчакайте операцията да приключи. Изтегляне е в ход. Изчакайте операцията да приключи. + Чакащи изтегляния + Няма чакащи изтегляния. + В момента се изтегля нещо + Преглед на напредъка + Чакащи изтегляния + %d в опашка + Няма diff --git a/controller/app/src/main/res/values-bn/strings_k2go.xml b/controller/app/src/main/res/values-bn/strings_k2go.xml index 5a3581e4..5c51ffd9 100644 --- a/controller/app/src/main/res/values-bn/strings_k2go.xml +++ b/controller/app/src/main/res/values-bn/strings_k2go.xml @@ -816,4 +816,11 @@ পুনরুদ্ধার চলছে। অনুগ্রহ করে অপারেশন শেষ হওয়া পর্যন্ত অপেক্ষা করুন। ইনস্টল চলছে। অনুগ্রহ করে অপারেশন শেষ হওয়া পর্যন্ত অপেক্ষা করুন। ডাউনলোড চলছে। অনুগ্রহ করে অপারেশন শেষ হওয়া পর্যন্ত অপেক্ষা করুন। + মুলতুবি ডাউনলোড + কোনো মুলতুবি ডাউনলোড নেই। + এখন কিছু ডাউনলোড হচ্ছে + অগ্রগতি দেখুন + মুলতুবি ডাউনলোড + সারিতে %d + কোনোটিই নয় diff --git a/controller/app/src/main/res/values-cs/strings_k2go.xml b/controller/app/src/main/res/values-cs/strings_k2go.xml index f04a4786..a277c888 100644 --- a/controller/app/src/main/res/values-cs/strings_k2go.xml +++ b/controller/app/src/main/res/values-cs/strings_k2go.xml @@ -816,4 +816,11 @@ Obnovení probíhá. Počkejte na dokončení operace. Instalace probíhá. Počkejte na dokončení operace. Stahování probíhá. Počkejte na dokončení operace. + Čekající stahování + Žádná čekající stahování. + Něco se právě stahuje + Zobrazit průběh + Čekající stahování + %d ve frontě + Žádné diff --git a/controller/app/src/main/res/values-de/strings_k2go.xml b/controller/app/src/main/res/values-de/strings_k2go.xml index 740d1c5c..9a49d5be 100644 --- a/controller/app/src/main/res/values-de/strings_k2go.xml +++ b/controller/app/src/main/res/values-de/strings_k2go.xml @@ -817,4 +817,11 @@ Eine Wiederherstellung läuft gerade. Bitte warten, bis der Vorgang abgeschlossen ist. Eine Installation läuft gerade. Bitte warten, bis der Vorgang abgeschlossen ist. Ein Download läuft gerade. Bitte warten, bis der Vorgang abgeschlossen ist. + Ausstehende Downloads + Keine ausstehenden Downloads. + Es wird gerade etwas heruntergeladen + Fortschritt anzeigen + Ausstehende Downloads + %d in Warteschlange + Keine diff --git a/controller/app/src/main/res/values-el/strings_k2go.xml b/controller/app/src/main/res/values-el/strings_k2go.xml index 7d03eaa7..d1e4898e 100644 --- a/controller/app/src/main/res/values-el/strings_k2go.xml +++ b/controller/app/src/main/res/values-el/strings_k2go.xml @@ -817,4 +817,11 @@ Μια επαναφορά βρίσκεται σε εξέλιξη. Περιμένετε να ολοκληρωθεί η λειτουργία. Μια εγκατάσταση βρίσκεται σε εξέλιξη. Περιμένετε να ολοκληρωθεί η λειτουργία. Μια λήψη βρίσκεται σε εξέλιξη. Περιμένετε να ολοκληρωθεί η λειτουργία. + Εκκρεμείς λήψεις + Δεν υπάρχουν εκκρεμείς λήψεις. + Γίνεται λήψη τώρα + Προβολή προόδου + Εκκρεμείς λήψεις + %d σε αναμονή + Καμία diff --git a/controller/app/src/main/res/values-es/strings_k2go.xml b/controller/app/src/main/res/values-es/strings_k2go.xml index f17249e1..384e3b33 100644 --- a/controller/app/src/main/res/values-es/strings_k2go.xml +++ b/controller/app/src/main/res/values-es/strings_k2go.xml @@ -816,4 +816,11 @@ Una restauración está en curso. Espera a que termine la operación. Una instalación está en curso. Espera a que termine la operación. Una descarga está en curso. Espera a que termine la operación. + Descargas pendientes + No hay descargas pendientes. + Se está descargando algo ahora + Ver progreso + Descargas pendientes + %d en cola + Ninguna diff --git a/controller/app/src/main/res/values-fa/strings_k2go.xml b/controller/app/src/main/res/values-fa/strings_k2go.xml index d30e9fbb..cf282636 100644 --- a/controller/app/src/main/res/values-fa/strings_k2go.xml +++ b/controller/app/src/main/res/values-fa/strings_k2go.xml @@ -816,4 +816,11 @@ بازیابی در حال انجام است. لطفاً تا پایان عملیات صبر کنید. نصب در حال انجام است. لطفاً تا پایان عملیات صبر کنید. دانلود در حال انجام است. لطفاً تا پایان عملیات صبر کنید. + دانلودهای در انتظار + دانلودی در انتظار نیست. + چیزی در حال دانلود است + مشاهده پیشرفت + دانلودهای در انتظار + %d در صف + هیچ diff --git a/controller/app/src/main/res/values-fr/strings_k2go.xml b/controller/app/src/main/res/values-fr/strings_k2go.xml index 24497fa5..e80345f3 100644 --- a/controller/app/src/main/res/values-fr/strings_k2go.xml +++ b/controller/app/src/main/res/values-fr/strings_k2go.xml @@ -826,4 +826,11 @@ Une restauration est en cours. Veuillez attendre la fin de l\'opération. Une installation est en cours. Veuillez attendre la fin de l\'opération. Un téléchargement est en cours. Veuillez attendre la fin de l\'opération. + Téléchargements en attente + Aucun téléchargement en attente. + Un téléchargement est en cours + Voir la progression + Téléchargements en attente + %d en file d\'attente + Aucun diff --git a/controller/app/src/main/res/values-gu/strings_k2go.xml b/controller/app/src/main/res/values-gu/strings_k2go.xml index e7d31a2c..c4a12c4a 100644 --- a/controller/app/src/main/res/values-gu/strings_k2go.xml +++ b/controller/app/src/main/res/values-gu/strings_k2go.xml @@ -816,4 +816,11 @@ પુનઃસ્થાપન ચાલી રહ્યું છે. કૃપા કરીને ક્રિયા પૂરી થાય ત્યાં સુધી રાહ જુઓ. ઇન્સ્ટૉલ ચાલી રહ્યું છે. કૃપા કરીને ક્રિયા પૂરી થાય ત્યાં સુધી રાહ જુઓ. ડાઉનલોડ ચાલી રહ્યું છે. કૃપા કરીને ક્રિયા પૂરી થાય ત્યાં સુધી રાહ જુઓ. + બાકી ડાઉનલોડ્સ + કોઈ બાકી ડાઉનલોડ નથી. + અત્યારે કંઈક ડાઉનલોડ થઈ રહ્યું છે + પ્રગતિ જુઓ + બાકી ડાઉનલોડ્સ + %d કતારમાં + કોઈ નહીં diff --git a/controller/app/src/main/res/values-hi/strings_k2go.xml b/controller/app/src/main/res/values-hi/strings_k2go.xml index b4fd28c5..eed23894 100644 --- a/controller/app/src/main/res/values-hi/strings_k2go.xml +++ b/controller/app/src/main/res/values-hi/strings_k2go.xml @@ -816,4 +816,11 @@ रिस्टोर चल रहा है। कृपया कार्य पूरा होने तक प्रतीक्षा करें। इंस्टॉल चल रहा है। कृपया कार्य पूरा होने तक प्रतीक्षा करें। डाउनलोड चल रहा है। कृपया कार्य पूरा होने तक प्रतीक्षा करें। + लंबित डाउनलोड + कोई लंबित डाउनलोड नहीं। + अभी कुछ डाउनलोड हो रहा है + प्रगति देखें + लंबित डाउनलोड + %d कतार में + कोई नहीं diff --git a/controller/app/src/main/res/values-hu/strings_k2go.xml b/controller/app/src/main/res/values-hu/strings_k2go.xml index c9830ac7..7528a7a0 100644 --- a/controller/app/src/main/res/values-hu/strings_k2go.xml +++ b/controller/app/src/main/res/values-hu/strings_k2go.xml @@ -816,4 +816,11 @@ Visszaállítás folyamatban van. Várja meg a művelet befejezését. Telepítés folyamatban van. Várja meg a művelet befejezését. Letöltés folyamatban van. Várja meg a művelet befejezését. + Függőben lévő letöltések + Nincs függőben lévő letöltés. + Most letöltődik valami + Folyamat megtekintése + Függőben lévő letöltések + %d a sorban + Nincs diff --git a/controller/app/src/main/res/values-in/strings_k2go.xml b/controller/app/src/main/res/values-in/strings_k2go.xml index 942b7c46..70115b74 100644 --- a/controller/app/src/main/res/values-in/strings_k2go.xml +++ b/controller/app/src/main/res/values-in/strings_k2go.xml @@ -816,4 +816,11 @@ Pemulihan sedang berlangsung. Harap tunggu hingga operasi selesai. Pemasangan sedang berlangsung. Harap tunggu hingga operasi selesai. Pengunduhan sedang berlangsung. Harap tunggu hingga operasi selesai. + Unduhan tertunda + Tidak ada unduhan tertunda. + Sesuatu sedang diunduh sekarang + Lihat progres + Unduhan tertunda + %d dalam antrean + Tidak ada diff --git a/controller/app/src/main/res/values-it/strings_k2go.xml b/controller/app/src/main/res/values-it/strings_k2go.xml index 281de658..9446eb18 100644 --- a/controller/app/src/main/res/values-it/strings_k2go.xml +++ b/controller/app/src/main/res/values-it/strings_k2go.xml @@ -816,4 +816,11 @@ Un ripristino è in corso. Attendi il completamento dell\'operazione. Un\'installazione è in corso. Attendi il completamento dell\'operazione. Un download è in corso. Attendi il completamento dell\'operazione. + Download in sospeso + Nessun download in sospeso. + È in corso un download + Mostra avanzamento + Download in sospeso + %d in coda + Nessuno diff --git a/controller/app/src/main/res/values-ja/strings_k2go.xml b/controller/app/src/main/res/values-ja/strings_k2go.xml index 2fe913d9..b8358c0a 100644 --- a/controller/app/src/main/res/values-ja/strings_k2go.xml +++ b/controller/app/src/main/res/values-ja/strings_k2go.xml @@ -816,4 +816,11 @@ 復元が進行中です。操作が完了するまでお待ちください。 インストールが進行中です。操作が完了するまでお待ちください。 ダウンロードが進行中です。操作が完了するまでお待ちください。 + 保留中のダウンロード + 保留中のダウンロードはありません。 + 現在ダウンロード中です + 進行状況を表示 + 保留中のダウンロード + %d 件待機中 + なし diff --git a/controller/app/src/main/res/values-ko/strings_k2go.xml b/controller/app/src/main/res/values-ko/strings_k2go.xml index a354223f..e795dc71 100644 --- a/controller/app/src/main/res/values-ko/strings_k2go.xml +++ b/controller/app/src/main/res/values-ko/strings_k2go.xml @@ -816,4 +816,11 @@ 복원이(가) 진행 중입니다. 작업이 완료될 때까지 기다려 주세요. 설치이(가) 진행 중입니다. 작업이 완료될 때까지 기다려 주세요. 다운로드이(가) 진행 중입니다. 작업이 완료될 때까지 기다려 주세요. + 대기 중인 다운로드 + 대기 중인 다운로드가 없습니다. + 지금 다운로드 중입니다 + 진행 상황 보기 + 대기 중인 다운로드 + %d개 대기 중 + 없음 diff --git a/controller/app/src/main/res/values-lt/strings_k2go.xml b/controller/app/src/main/res/values-lt/strings_k2go.xml index 342ba314..e5b6ad03 100644 --- a/controller/app/src/main/res/values-lt/strings_k2go.xml +++ b/controller/app/src/main/res/values-lt/strings_k2go.xml @@ -826,4 +826,11 @@ Atkūrimas vyksta. Palaukite, kol operacija bus baigta. Diegimas vyksta. Palaukite, kol operacija bus baigta. Atsisiuntimas vyksta. Palaukite, kol operacija bus baigta. + Laukiantys atsisiuntimai + Nėra laukiančių atsisiuntimų. + Šiuo metu kažkas atsisiunčiama + Peržiūrėti eigą + Laukiantys atsisiuntimai + %d eilėje + Nėra diff --git a/controller/app/src/main/res/values-nl/strings_k2go.xml b/controller/app/src/main/res/values-nl/strings_k2go.xml index 7ec93130..9a27a5d9 100644 --- a/controller/app/src/main/res/values-nl/strings_k2go.xml +++ b/controller/app/src/main/res/values-nl/strings_k2go.xml @@ -816,4 +816,11 @@ Een herstel is bezig. Wacht tot de bewerking klaar is. Een installatie is bezig. Wacht tot de bewerking klaar is. Een download is bezig. Wacht tot de bewerking klaar is. + Downloads in wachtrij + Geen downloads in wachtrij. + Er wordt nu iets gedownload + Voortgang bekijken + Downloads in wachtrij + %d in wachtrij + Geen diff --git a/controller/app/src/main/res/values-no/strings_k2go.xml b/controller/app/src/main/res/values-no/strings_k2go.xml index e0f568a9..3b17513f 100644 --- a/controller/app/src/main/res/values-no/strings_k2go.xml +++ b/controller/app/src/main/res/values-no/strings_k2go.xml @@ -826,4 +826,11 @@ En gjenoppretting pågår. Vent til operasjonen er fullført. En installasjon pågår. Vent til operasjonen er fullført. En nedlasting pågår. Vent til operasjonen er fullført. + Ventende nedlastinger + Ingen ventende nedlastinger. + Noe lastes ned nå + Se fremdrift + Ventende nedlastinger + %d i kø + Ingen diff --git a/controller/app/src/main/res/values-pl/strings_k2go.xml b/controller/app/src/main/res/values-pl/strings_k2go.xml index e2bfb3af..caaeb640 100644 --- a/controller/app/src/main/res/values-pl/strings_k2go.xml +++ b/controller/app/src/main/res/values-pl/strings_k2go.xml @@ -826,4 +826,11 @@ Przywracanie jest w toku. Poczekaj na zakończenie operacji. Instalacja jest w toku. Poczekaj na zakończenie operacji. Pobieranie jest w toku. Poczekaj na zakończenie operacji. + Oczekujące pobierania + Brak oczekujących pobierań. + Coś jest teraz pobierane + Zobacz postęp + Oczekujące pobierania + %d w kolejce + Brak diff --git a/controller/app/src/main/res/values-pt/strings_k2go.xml b/controller/app/src/main/res/values-pt/strings_k2go.xml index fd45bb32..d63c7c08 100644 --- a/controller/app/src/main/res/values-pt/strings_k2go.xml +++ b/controller/app/src/main/res/values-pt/strings_k2go.xml @@ -816,4 +816,11 @@ Uma restauração está em andamento. Aguarde a conclusão da operação. Uma instalação está em andamento. Aguarde a conclusão da operação. Uma transferência está em andamento. Aguarde a conclusão da operação. + Downloads pendentes + Nenhum download pendente. + Algo está sendo baixado agora + Ver progresso + Downloads pendentes + %d na fila + Nenhum diff --git a/controller/app/src/main/res/values-ro/strings_k2go.xml b/controller/app/src/main/res/values-ro/strings_k2go.xml index b281fa2b..7b83f16a 100644 --- a/controller/app/src/main/res/values-ro/strings_k2go.xml +++ b/controller/app/src/main/res/values-ro/strings_k2go.xml @@ -816,4 +816,11 @@ O restaurare este în curs. Așteptați finalizarea operațiunii. O instalare este în curs. Așteptați finalizarea operațiunii. O descărcare este în curs. Așteptați finalizarea operațiunii. + Descărcări în așteptare + Nicio descărcare în așteptare. + Se descarcă ceva acum + Vezi progresul + Descărcări în așteptare + %d în coadă + Niciuna diff --git a/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml b/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml index 59f7202e..63c281c6 100644 --- a/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml +++ b/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml @@ -816,4 +816,11 @@ Восстановление выполняется. Дождитесь завершения операции. Установка выполняется. Дождитесь завершения операции. Загрузка выполняется. Дождитесь завершения операции. + Ожидающие загрузки + Нет ожидающих загрузок. + Сейчас что-то загружается + Показать ход + Ожидающие загрузки + %d в очереди + Нет diff --git a/controller/app/src/main/res/values-sk/strings_k2go.xml b/controller/app/src/main/res/values-sk/strings_k2go.xml index c6de7a9d..6b4cf25e 100644 --- a/controller/app/src/main/res/values-sk/strings_k2go.xml +++ b/controller/app/src/main/res/values-sk/strings_k2go.xml @@ -816,4 +816,11 @@ Obnovenie prebieha. Počkajte na dokončenie operácie. Inštalácia prebieha. Počkajte na dokončenie operácie. Sťahovanie prebieha. Počkajte na dokončenie operácie. + Čakajúce sťahovania + Žiadne čakajúce sťahovania. + Práve sa niečo sťahuje + Zobraziť priebeh + Čakajúce sťahovania + %d v poradí + Žiadne diff --git a/controller/app/src/main/res/values-sr/strings_k2go.xml b/controller/app/src/main/res/values-sr/strings_k2go.xml index c52f0a2f..64e5f9ad 100644 --- a/controller/app/src/main/res/values-sr/strings_k2go.xml +++ b/controller/app/src/main/res/values-sr/strings_k2go.xml @@ -816,4 +816,11 @@ Враћање је у току. Сачекајте да се операција заврши. Инсталација је у току. Сачекајте да се операција заврши. Преузимање је у току. Сачекајте да се операција заврши. + Преузимања на чекању + Нема преузимања на чекању. + Нешто се сада преузима + Прикажи напредак + Преузимања на чекању + %d у реду + Нема diff --git a/controller/app/src/main/res/values-sw/strings_k2go.xml b/controller/app/src/main/res/values-sw/strings_k2go.xml index 4cd29b94..58a15270 100644 --- a/controller/app/src/main/res/values-sw/strings_k2go.xml +++ b/controller/app/src/main/res/values-sw/strings_k2go.xml @@ -816,4 +816,11 @@ Kurejesha inaendelea. Tafadhali subiri operesheni ikamilike. Usakinishaji inaendelea. Tafadhali subiri operesheni ikamilike. Upakuaji inaendelea. Tafadhali subiri operesheni ikamilike. + Vipakuliwa vinavyosubiri + Hakuna vipakuliwa vinavyosubiri. + Kitu kinapakuliwa sasa + Ona maendeleo + Vipakuliwa vinavyosubiri + %d kwenye foleni + Hakuna diff --git a/controller/app/src/main/res/values-ta/strings_k2go.xml b/controller/app/src/main/res/values-ta/strings_k2go.xml index 7aaa9467..18052391 100644 --- a/controller/app/src/main/res/values-ta/strings_k2go.xml +++ b/controller/app/src/main/res/values-ta/strings_k2go.xml @@ -816,4 +816,11 @@ மீட்டமைப்பு நடைபெறுகிறது. செயல்பாடு முடியும் வரை காத்திருக்கவும். நிறுவல் நடைபெறுகிறது. செயல்பாடு முடியும் வரை காத்திருக்கவும். பதிவிறக்கம் நடைபெறுகிறது. செயல்பாடு முடியும் வரை காத்திருக்கவும். + நிலுவையிலுள்ள பதிவிறக்கங்கள் + நிலுவையில் பதிவிறக்கங்கள் இல்லை. + இப்போது ஏதோ பதிவிறக்கப்படுகிறது + முன்னேற்றத்தைக் காண்க + நிலுவையிலுள்ள பதிவிறக்கங்கள் + வரிசையில் %d + எதுவுமில்லை diff --git a/controller/app/src/main/res/values-tr/strings_k2go.xml b/controller/app/src/main/res/values-tr/strings_k2go.xml index 32ad8f70..c8d3b1c5 100644 --- a/controller/app/src/main/res/values-tr/strings_k2go.xml +++ b/controller/app/src/main/res/values-tr/strings_k2go.xml @@ -816,4 +816,11 @@ Geri yükleme sürüyor. Lütfen işlemin bitmesini bekleyin. Kurulum sürüyor. Lütfen işlemin bitmesini bekleyin. İndirme sürüyor. Lütfen işlemin bitmesini bekleyin. + Bekleyen indirmeler + Bekleyen indirme yok. + Şu anda bir şey indiriliyor + İlerlemeyi gör + Bekleyen indirmeler + Sırada %d + Yok diff --git a/controller/app/src/main/res/values-uk/strings_k2go.xml b/controller/app/src/main/res/values-uk/strings_k2go.xml index f3ee7a5b..57e3573b 100644 --- a/controller/app/src/main/res/values-uk/strings_k2go.xml +++ b/controller/app/src/main/res/values-uk/strings_k2go.xml @@ -816,4 +816,11 @@ Відновлення виконується. Зачекайте завершення операції. Встановлення виконується. Зачекайте завершення операції. Завантаження виконується. Зачекайте завершення операції. + Завантаження в черзі + Немає завантажень у черзі. + Зараз щось завантажується + Переглянути прогрес + Завантаження в черзі + %d у черзі + Немає diff --git a/controller/app/src/main/res/values-vi/strings_k2go.xml b/controller/app/src/main/res/values-vi/strings_k2go.xml index 54ca082c..f551f416 100644 --- a/controller/app/src/main/res/values-vi/strings_k2go.xml +++ b/controller/app/src/main/res/values-vi/strings_k2go.xml @@ -816,4 +816,11 @@ Một lần khôi phục đang diễn ra. Vui lòng đợi thao tác hoàn tất. Một lần cài đặt đang diễn ra. Vui lòng đợi thao tác hoàn tất. Một lượt tải xuống đang diễn ra. Vui lòng đợi thao tác hoàn tất. + Tải xuống đang chờ + Không có tải xuống đang chờ. + Đang tải xuống một mục + Xem tiến trình + Tải xuống đang chờ + %d trong hàng đợi + Không có diff --git a/controller/app/src/main/res/values-yo/strings_k2go.xml b/controller/app/src/main/res/values-yo/strings_k2go.xml index 4ab244e5..aa0bcd00 100644 --- a/controller/app/src/main/res/values-yo/strings_k2go.xml +++ b/controller/app/src/main/res/values-yo/strings_k2go.xml @@ -816,4 +816,11 @@ Ìmúpadàbọ̀ ń lọ lọ́wọ́. Jọ̀wọ́ dúró kí iṣẹ́ náà parí. Fifi-sórí-ẹrọ ń lọ lọ́wọ́. Jọ̀wọ́ dúró kí iṣẹ́ náà parí. Ìgbàsílẹ̀ ń lọ lọ́wọ́. Jọ̀wọ́ dúró kí iṣẹ́ náà parí. + Àwọn ìgbàsílẹ̀ tí ń dúró + Kò sí ìgbàsílẹ̀ tí ń dúró. + Ohun kan ń gbà sílẹ̀ báyìí + Wo ìtẹ̀síwájú + Àwọn ìgbàsílẹ̀ tí ń dúró + %d ní ìlà + Kò sí diff --git a/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml b/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml index 17fc6b9e..455b6d75 100644 --- a/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml +++ b/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml @@ -816,4 +816,11 @@ 恢复正在进行中。请等待操作完成。 安装正在进行中。请等待操作完成。 下载正在进行中。请等待操作完成。 + 待处理的下载 + 没有待处理的下载。 + 正在下载 + 查看进度 + 待处理的下载 + %d 个排队中 + From 3c6719fb911870ea129064d5fc725ad078fc200e Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 22:11:18 -0600 Subject: [PATCH 10/12] ADFA-5169 test(pending): instrumented test for the banked list + per-item cancel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PendingOrdersRepositoryImplTest (androidTest) validates finding 6 deterministically with real SharedPreferences: the three content wishlists list as PendingOrders grouped by type, the empty state, and per-item cancel — including cancelling one ZIM and leaving the other, which exercises the new ZimWishlist.remove. The design doc notes why this stands in for a banked state not reachable through normal UX. --- .../data/PendingOrdersRepositoryImplTest.java | 130 ++++++++++++++++++ .../ADFA-5169-pending-downloads-design.md | 40 ++++++ 2 files changed, 170 insertions(+) create mode 100644 controller/app/src/androidTest/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImplTest.java diff --git a/controller/app/src/androidTest/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImplTest.java b/controller/app/src/androidTest/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImplTest.java new file mode 100644 index 00000000..960b62f8 --- /dev/null +++ b/controller/app/src/androidTest/java/org/iiab/controller/pending/data/PendingOrdersRepositoryImplTest.java @@ -0,0 +1,130 @@ +/* + * ============================================================================ + * Name : PendingOrdersRepositoryImplTest.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Instrumented test for ADFA-5169 (finding 6). Validates the banked- + * order mechanism end to end at the data layer with real + * SharedPreferences: the three content wishlists are listed as + * PendingOrders, and cancelling one removes only that order. This is + * the deterministic stand-in for a state that is not reachable through + * normal UX (see controller/docs/ADFA-5169-pending-downloads-design.md). + * ============================================================================ + */ +package org.iiab.controller.pending.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.iiab.controller.kolibri.data.KolibriWishlist; +import org.iiab.controller.pending.domain.PendingOrder; +import org.iiab.controller.redesign.BooksWishlist; +import org.iiab.controller.redesign.ZimWishlist; +import org.iiab.controller.system.domain.ContentType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class PendingOrdersRepositoryImplTest { + + /** A valid Kolibri channel id: 32 lowercase hex chars (ChannelId.normalise). */ + private static final String CHANNEL = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"; + + private Context ctx() { + return ApplicationProvider.getApplicationContext(); + } + + @Before + @After + public void clearWishlists() { + Context c = ctx(); + ZimWishlist.clear(c); + BooksWishlist.clear(c); + KolibriWishlist.clear(c); + } + + private void seedFour() { + Context c = ctx(); + Map zim = new LinkedHashMap<>(); + zim.put("wikipedia|en|maxi", 4_200L); + zim.put("wikipedia|es|maxi", 3_100L); + ZimWishlist.add(c, zim); + BooksWishlist.add(c, "b1", "Gutenberg", "https://example.invalid/x.epub"); + KolibriWishlist.add(c, CHANNEL, 1, "Khan Academy", 12_000L, null); + } + + @Test + public void emptyWhenNothingBanked() { + assertTrue(new PendingOrdersRepositoryImpl(ctx()).list().isEmpty()); + } + + @Test + public void listsEveryBankedOrderGroupedByType() { + seedFour(); + List orders = new PendingOrdersRepositoryImpl(ctx()).list(); + + assertEquals(4, orders.size()); + // Grouped by content type in enum order: ZIM (2), Books (1), Courses (1). + assertEquals(ContentType.ZIM, orders.get(0).type()); + assertEquals(ContentType.ZIM, orders.get(1).type()); + assertEquals(ContentType.BOOKS, orders.get(2).type()); + assertEquals(ContentType.COURSES, orders.get(3).type()); + // Names: ZIM uses its selector key; Books uses the title; Courses uses the name. + assertEquals("wikipedia|en|maxi", orders.get(0).name()); + assertEquals("Gutenberg", orders.get(2).name()); + assertEquals("Khan Academy", orders.get(3).name()); + } + + @Test + public void cancelRemovesOnlyThatOrder() { + seedFour(); + PendingOrdersRepositoryImpl repo = new PendingOrdersRepositoryImpl(ctx()); + + PendingOrder book = null; + for (PendingOrder o : repo.list()) { + if (o.type() == ContentType.BOOKS) book = o; + } + assertNotNull(book); + repo.cancel(book); + + List after = repo.list(); + assertEquals(3, after.size()); + for (PendingOrder o : after) { + assertNotEquals(ContentType.BOOKS, o.type()); + } + assertEquals(0, BooksWishlist.size(ctx())); // the Books order is gone + assertEquals(2, ZimWishlist.size(ctx())); // the rest are untouched + assertEquals(1, KolibriWishlist.size(ctx())); + } + + @Test + public void cancelOneZimLeavesTheOtherZim() { + seedFour(); + PendingOrdersRepositoryImpl repo = new PendingOrdersRepositoryImpl(ctx()); + + // Exercises the new ZimWishlist.remove(key): drop only the English collection. + repo.cancel(new PendingOrder(ContentType.ZIM, "wikipedia|en|maxi", "x", 0L)); + + List after = repo.list(); + assertEquals(3, after.size()); + assertEquals(1, ZimWishlist.size(ctx())); + boolean spanishRemains = false; + for (PendingOrder o : after) { + if ("wikipedia|es|maxi".equals(o.id())) spanishRemains = true; + } + assertTrue(spanishRemains); + } +} diff --git a/controller/docs/ADFA-5169-pending-downloads-design.md b/controller/docs/ADFA-5169-pending-downloads-design.md index fe669f22..52d28d59 100644 --- a/controller/docs/ADFA-5169-pending-downloads-design.md +++ b/controller/docs/ADFA-5169-pending-downloads-design.md @@ -119,6 +119,46 @@ with a snackbar. No automatic action anywhere. - New user-facing strings ship in all 33 locales in the same PR (a missing translation fails the build). +## Validation (and why the "natural" repro does not exist) + +The durably-banked state is not reachable through normal UX, which shapes how this is +validated. Confirmed in code: + +- Live mode (Get More, with a system): `SetupLibraryActivity.startZimDownload()` / + `startBooksDownload()` drain immediately (`…Provisioner.drain`), so content never + stays banked there — it starts at once (or moves into the download service). +- Wizard mode (pre-install): `zimWizardConfirm()` / `booksWizardConfirm()` bank the + selection without draining, but the wizard has no route to Settings, and completing + the install lets the Home pump drain the banked content. +- A systemless device opens the wizard, not the tabs, so Settings is unreachable there. + +So the states that keep an order banked (no system, or the in-server REST engine down +while nginx still answers) are incompatible with reaching the Settings screen that +shows it. The only durable case — rootfs present but the REST engine down, so the Home +pump's `apiReady()` gate never opens — requires stopping just that engine inside the +container and is not a user path. + +Validation is therefore: + +1. Automated, deterministic — an instrumented test (`PendingOrdersRepositoryImplTest`, + src/androidTest) seeds the three wishlists with real SharedPreferences and asserts + the banked mechanism: the orders are listed grouped by type with the right names, + the empty state, and per-item cancel (cancelling one removes only that order — + including a ZIM cancel leaving the other ZIM, which exercises the new + `ZimWishlist.remove`). Run with `./gradlew :app:connectedDebugAndroidTest`. +2. The UI end to end — the list, per-item cancel, nothing cancelled automatically, and + the Settings count refreshing on entry and on return — via a debug seed that writes + the same banked state (`DebugSeedPendingReceiver`, src/debug); stay off Home so the + pump does not drain it. Same kind of stand-in as the ADFA-5146 stale flag. +3. That real banking occurs through the UI is confirmed by selecting content in the + wizard and inspecting the wishlist: + `adb shell run-as org.iiab.controller cat shared_prefs/k2go_zim_wishlist.xml`. +4. The "See progress" link is validated with a real running download; the maps/module + cancel paths are checked directly for regressions. + +Not left pending: a continuous natural flow to a durably-banked order reached from +Settings does not exist by construction; it is covered by (1) + (2). + ## Seams / reuse - Reuse `ContentType` + `PendingContent`; no duplicated wishlist knowledge. From a85361b59a6e1c484c259f9b8cdf4ce847c934bd Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 22:20:10 -0600 Subject: [PATCH 11/12] ADFA-5169 chore(pending): remove the debug seed hook Drops DebugSeedPendingReceiver and its debug-manifest entry, used to validate the Pending downloads screen from adb. The instrumented test (PendingOrdersRepositoryImplTest) now covers the mechanism, so the hook is no longer needed. --- controller/app/src/debug/AndroidManifest.xml | 9 ---- .../debug/DebugSeedPendingReceiver.java | 54 ------------------- 2 files changed, 63 deletions(-) delete mode 100644 controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java diff --git a/controller/app/src/debug/AndroidManifest.xml b/controller/app/src/debug/AndroidManifest.xml index d1e3d319..200c0910 100644 --- a/controller/app/src/debug/AndroidManifest.xml +++ b/controller/app/src/debug/AndroidManifest.xml @@ -12,14 +12,5 @@ - - - - - - diff --git a/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java b/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java deleted file mode 100644 index fa09499a..00000000 --- a/controller/app/src/debug/java/org/iiab/controller/pending/debug/DebugSeedPendingReceiver.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.iiab.controller.pending.debug; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.widget.Toast; - -import org.iiab.controller.kolibri.data.KolibriWishlist; -import org.iiab.controller.redesign.BooksWishlist; -import org.iiab.controller.redesign.ZimWishlist; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * DEBUG-ONLY (lives in src/debug; never ships in release). Seeds a few banked content - * orders — two ZIM, one Books, one Courses — so the Pending downloads screen (ADFA-5169, - * finding 6) can be validated without arranging a real deferred drain. - * - *
- * adb shell am broadcast \
- *   -a org.iiab.controller.DEBUG_SEED_PENDING \
- *   -n org.iiab.controller/org.iiab.controller.pending.debug.DebugSeedPendingReceiver
- * 
- * - * Then open Settings > Pending downloads to see them and Cancel each. Stay off the Home - * tab while validating: Home's pump drains the wishlists on a live system, which would - * clear the seed. Watch {@code adb logcat -s IIAB-DebugSeedPending}. Remove before merge. - */ -public final class DebugSeedPendingReceiver extends BroadcastReceiver { - - private static final String TAG = "IIAB-DebugSeedPending"; - - @Override - public void onReceive(Context context, Intent intent) { - Context app = context.getApplicationContext(); - - Map zim = new LinkedHashMap<>(); - zim.put("wikipedia|en|maxi", 4_200_000_000L); - zim.put("wikipedia|es|maxi", 3_100_000_000L); - ZimWishlist.add(app, zim); - - BooksWishlist.add(app, "gutenberg-fiction", "Gutenberg — Fiction", - "https://example.invalid/fiction.epub"); - - KolibriWishlist.add(app, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", 1, - "Khan Academy — Math", 12_000_000_000L, null); - - Log.i(TAG, "Seeded pending orders: 2 ZIM, 1 Books, 1 Courses. " - + "Open Settings > Pending downloads (stay off Home)."); - Toast.makeText(app, "Debug: seeded 4 pending orders", Toast.LENGTH_SHORT).show(); - } -} From 95bc06ec4c59013657114426a444c2d0f512ac21 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Mon, 17 Aug 2026 22:43:16 -0600 Subject: [PATCH 12/12] ADFA-5169 docs(diagrams): close finding 6 and reconcile ticket statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit state-spine: finding 6 → closed (ADFA-5169); its state node S12 → resolved; the "module sheet" dead control → resolved (ADFA-5104 made it honest); header note refreshed to "all eight findings closed". roadmap: ADFA-5158 marked landed (was "in flight"); verified every other ticket card against Jira under epic 1028 — 5094 and the open cards remain open, the DONE cards remain done. --- controller/docs/operation-model-roadmap.svg | 2 +- controller/docs/state-spine.svg | 46 ++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/controller/docs/operation-model-roadmap.svg b/controller/docs/operation-model-roadmap.svg index 89221e88..649f80ec 100644 --- a/controller/docs/operation-model-roadmap.svg +++ b/controller/docs/operation-model-roadmap.svg @@ -432,7 +432,7 @@ The receive/clone dead-ends closed — and the debt the fixes exposed Landed and merged: ADFA-5143 (a clone in flight has a name and a way back), 5147 (a damaged system reads as damaged), 5150 (systemless Connect/Send route to Recover), 5151 (confine the receive loop to the recover hub), 5152 (a reopen lands on progress, not the fork), 5154 (Connect + Clone Send redesigned to stacked / 2-page QR), 5157 (shared QrSection), -5155 (receive success waits for services before Home, never a fixed timer). In flight: ADFA-5158 (get-app QR waits for the hotspot AP IP by polling, not a one-shot; broadened detection). +5155 (receive success waits for services before Home, never a fixed timer). Also landed: ADFA-5158 (get-app QR waits for the hotspot AP IP by polling, not a one-shot; broadened detection). FOLLOW-UPS WORTH A TICKET Shared "poll until a resource is ready" utility (a ServiceReadyGate). The same Handler-reschedule + in-flight guard + teardown now lives in three places — SetupProgressActivity.readyPoll, diff --git a/controller/docs/state-spine.svg b/controller/docs/state-spine.svg index a974ec7f..29baedb0 100644 --- a/controller/docs/state-spine.svg +++ b/controller/docs/state-spine.svg @@ -40,7 +40,7 @@ System lifecycle — where a user gets stuck -A state is a combination of facts, never a screen. Verified against the working tree, 15 Aug — findings 4 and 7 closed since (ADFA-5147, ADFA-5150). Every edge cites the guard that allows it. +A state is a combination of facts, never a screen. Verified against the working tree, 17 Aug — all eight findings below are now closed (the last, 6, by ADFA-5169). Every edge cites the guard that allows it. LEGEND @@ -107,13 +107,13 @@ the header keeps its Install button. Gate is 900 ms here, not 25 s · :317 - -Install → refused + +Install → "needs a system" - -Dead control · module sheet -Install routes to the module hub, which -then refuses · ModuleHubFragment:233 + +Module sheet · now honest +ADFA-5104: no system → "needs a system", +not a silent refuse · residue: S1's Install Get more @@ -164,16 +164,16 @@ drain refused or deferred - -S12 · Order banked, nothing running + +S12 · Order banked — now surfaced anyBanked && !anyRunning -Looks exactly like S9. No "See -progress", no route to the index, -no list of what was ordered. -LibraryHomeFragment:518-529 -A banked ZIM, Books or Courses order -has no cancel anywhere: isScheduled() -only looks at maps and modules · :337 +Was: looked exactly like S9 — no +"See progress", no list, no cancel +for a ZIM/Books/Courses order. +org.iiab.controller.pending · PendingOrdersFragment +ADFA-5169: a persistent Settings › +Pending downloads lists every order +and cancels each by item, never auto. @@ -222,7 +222,7 @@ DEAD ENDS, WORST FIRST — traps, then one-way doors, then exits nobody can find, then controls that do nothing -Ranked by how hard the state is to escape, not by how ugly it looks. As of 17 Aug: findings 1, 2, 3, 4, 5, 7 and 8 are closed. One remains: 6, a banked order with no home and no owner yet. +Ranked by how hard the state is to escape, not by how ugly it looks. As of 17 Aug all eight findings are closed — 6, the last, by ADFA-5169. None remain. Findings 1–4 were one sentence four times: a fact nobody clears. 1, 3 and 4 were closed first; 2 — the release blocker, a row in the standing directive's ledger at the top of operation-model-roadmap.svg — is now closed too (ADFA-5146, 17 Aug). @@ -272,12 +272,12 @@ was already alive. A state with no exit is a bug whoever reaches it, including by a route that does not exist yet. LibraryHomeFragment:111 (the action), :252-275 (openGetMore — re-read + ownerHeld guard) · LibraryActivity:112-127 (the launch check) · ModuleHubFragment:232-243. - -6 · DEAD CONTROL — a banked order that is not running is invisible and, for three types, uncancellable -"See progress" appears only while something is actually running, and a refused or deferred drain leaves the order banked without running. Nothing lists it. The module -sheet's Cancel is the only cancel in the app, and it only inspects the maps and module wishlists — a banked ZIM, Books or Courses order cannot be cancelled from -anywhere in the UI. -LibraryHomeFragment.java:518-529 (anyRunning gates the button), :337-342 (isScheduled ignores three of the five wishlists). + +6 · CLOSED (17 Aug, ADFA-5169) — a queued order is now visible and cancellable from Settings +Was: "See progress" appeared only while something ran, and a refused or deferred drain left the order banked without running, listed nowhere; the module sheet's Cancel +inspected only the maps and module wishlists, so a banked ZIM, Books or Courses order could not be cancelled from anywhere in the UI. +Now a persistent Settings › Pending downloads screen lists every queued order by item and cancels each one; nothing is cancelled or purged automatically. +org.iiab.controller.pending (domain/data/presentation) · Settings row → PendingOrdersFragment · ZimWishlist.remove · reuses ContentType/PendingContent · instrumented test PendingOrdersRepositoryImplTest. 7 · CLOSED (ADFA-5150) — Connect and Clone Send read the system fact and offer Recover, not a dead QR