@@ -80,6 +80,54 @@ public static void wipeCache(Context context) {
8080 if (cacheFile .exists ()) cacheFile .delete ();
8181 }
8282
83+ /** Canonical Wikipedia variants, ordered coverage-desc then detail-desc. */
84+ public static final String [] CANONICAL_VARIANTS = {
85+ "all_maxi" , "all_nopic" , "all_mini" ,
86+ "top1m_maxi" , "top1m_nopic" , "top1m_mini" ,
87+ "top_maxi" , "top_nopic" , "top_mini"
88+ };
89+
90+ /** Variants actually present for a language, in canonical order (for the UI to render). */
91+ public static java .util .List <String > availableVariants (JSONObject langData ) {
92+ java .util .List <String > out = new java .util .ArrayList <>();
93+ if (langData == null ) return out ;
94+ for (String key : CANONICAL_VARIANTS ) if (langData .has (key )) out .add (key );
95+ return out ;
96+ }
97+
98+ private static String coverageOf (String variant ) {
99+ if (variant .startsWith ("all_" )) return "all" ;
100+ if (variant .startsWith ("top1m_" )) return "top1m" ;
101+ return "top" ;
102+ }
103+ private static String detailOf (String variant ) {
104+ if (variant .endsWith ("_maxi" )) return "maxi" ;
105+ if (variant .endsWith ("_mini" )) return "mini" ;
106+ return "nopic" ;
107+ }
108+
109+ /**
110+ * Resolve an explicit variant against what the language actually offers, degrading only
111+ * DOWNWARD within the coverage family (top1m -> top) and across detail, never escalating.
112+ * Returns a present key or null.
113+ */
114+ private static String resolveInFamily (JSONObject langData , String requested ) {
115+ if (langData == null ) return null ;
116+ if (langData .has (requested )) return requested ;
117+ String cov = coverageOf (requested );
118+ String det = detailOf (requested );
119+ String [] covLadder = cov .equals ("all" ) ? new String []{"all" }
120+ : cov .equals ("top1m" ) ? new String []{"top1m" , "top" }
121+ : new String []{"top" };
122+ String [] detLadder = det .equals ("maxi" ) ? new String []{"maxi" , "nopic" , "mini" }
123+ : det .equals ("nopic" ) ? new String []{"nopic" , "mini" , "maxi" }
124+ : new String []{"mini" , "nopic" , "maxi" };
125+ for (String cc : covLadder )
126+ for (String dd : detLadder )
127+ if (langData .has (cc + "_" + dd )) return cc + "_" + dd ;
128+ return null ;
129+ }
130+
83131 public static void getOrFetchCatalog (Context context , CacheListener listener ) {
84132 new Thread (() -> {
85133 File cacheFile = new File (context .getCacheDir (), CACHE_FILE_NAME );
@@ -134,12 +182,19 @@ public static void getOrFetchCatalog(Context context, CacheListener listener) {
134182
135183 if (!database .has (lang )) database .put (lang , new JSONObject ());
136184
137- // NEW: Store size AND exact filename
138- JSONObject variantData = new JSONObject ();
139- variantData .put ("size" , sizeGb );
140- variantData .put ("file" , "wikipedia_" + lang + "_" + variant + "_" + date + ".zim" );
141-
142- database .getJSONObject (lang ).put (variant , variantData );
185+ // Store size, exact filename AND date. The listing can hold more than
186+ // one date for the same variant; keep ONLY the newest by declared
187+ // intent ("YYYY-MM" compares lexicographically = chronologically),
188+ // not by whichever row happens to come last in the HTML.
189+ JSONObject langObj = database .getJSONObject (lang );
190+ JSONObject existing = langObj .optJSONObject (variant );
191+ if (existing == null || date .compareTo (existing .optString ("date" , "" )) > 0 ) {
192+ JSONObject variantData = new JSONObject ();
193+ variantData .put ("size" , sizeGb );
194+ variantData .put ("date" , date );
195+ variantData .put ("file" , "wikipedia_" + lang + "_" + variant + "_" + date + ".zim" );
196+ langObj .put (variant , variantData );
197+ }
143198 }
144199 }
145200
@@ -223,30 +278,40 @@ private static void computeProjection(Context context, Tier tier, boolean pullCo
223278 @ Override
224279 public void onReady (JSONObject catalog ) {
225280 double kiwixSize = 0.0 ;
226- String resolvedLang = langCode ;
281+ String resolvedLang = org . iiab . controller . applang . data . ContentLanguage . normalize ( langCode ) ;
227282 String resolvedFile = null ;
228283
229- JSONObject langData = catalog .optJSONObject (langCode );
284+ JSONObject langData = catalog .optJSONObject (resolvedLang );
230285
231286 if (langData == null ) {
232287 langData = catalog .optJSONObject ("en" );
233288 resolvedLang = "en" ;
234289 }
235290
236291 if (langData != null ) {
237- if (overrideVariant != null && langData .has (overrideVariant )) {
238- JSONObject vData = langData .optJSONObject (overrideVariant );
239- // VDATA NULL PROTECTION
240- if (vData != null ) {
241- kiwixSize = vData .optDouble ("size" , 0.0 );
242- resolvedFile = vData .optString ("file" , null );
292+ if (overrideVariant != null && overrideVariant .isEmpty ()) {
293+ // Explicit "no Wikipedia" (skip): leave kiwixSize 0 / resolvedFile
294+ // null. Skipping must mean ZERO — never a tier-coupled auto-pick.
295+ } else if (overrideVariant != null ) {
296+ // Explicit pick from the UI. Honor it exactly, or degrade WITHIN
297+ // the same coverage family (e.g. top1m_maxi -> top_maxi), NEVER
298+ // escalating — a "Popular" pick must not silently become the full
299+ // "Everything" ZIM. If nothing in-family exists, install no
300+ // Wikipedia rather than substituting a different corpus.
301+ String resolved = resolveInFamily (langData , overrideVariant );
302+ if (resolved != null ) {
303+ JSONObject vData = langData .optJSONObject (resolved );
304+ if (vData != null ) {
305+ kiwixSize = vData .optDouble ("size" , 0.0 );
306+ resolvedFile = vData .optString ("file" , null );
307+ }
243308 }
244309 } else {
310+ // Legacy callers with no explicit pick: priority auto-select.
245311 String [] priorities = {"all_maxi" , "top1m_maxi" , "all_nopic" , "top1m_nopic" , "top_maxi" , "all_mini" , "top_nopic" };
246312 for (String p : priorities ) {
247313 if (langData .has (p )) {
248314 JSONObject vData = langData .optJSONObject (p );
249- // VDATA NULL PROTECTION
250315 if (vData != null ) {
251316 double size = vData .optDouble ("size" , 0.0 );
252317 if (size <= finalLimit ) {
@@ -280,6 +345,12 @@ public void onError(String error) {
280345 * <p>Performs a (cached) network call; must run off the main thread — it does,
281346 * because every caller is inside {@link #calculateProjectedSize}'s worker thread.
282347 */
348+ /** No-network last-known OS size (RootfsCatalog fallback) — instant, for placeholders. */
349+ public static double fallbackOsSizeGb (Tier tier ) {
350+ RootfsCatalog catalog = new RootfsCatalog ();
351+ return ByteFormatter .toGiB (catalog .fallbackBytes (toDomainTier (tier ), catalog .detectAbi ()));
352+ }
353+
283354 private static double resolveOsSizeGb (Context context , Tier tier ) {
284355 RootfsCatalog catalog = new RootfsCatalog (context );
285356 RootfsRepository repository =
0 commit comments