Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class PortalActivity extends AppCompatActivity {

private GestureWebView webView;
private org.iiab.controller.redesign.FqrController fqr; // ADFA-4879: FQR maps (only on /maps/)
private org.iiab.controller.redesign.KiwixManageController kiwixMgr; // ADFA-5004: ZIM delete (only on /kiwix/)
private static final long AUTO_HIDE_MS = 4000L; // ADFA-4887: nav-bar auto-hide after inactivity
private boolean fullscreenOn = false; // ADFA-4887: Home button toggles fullscreen
private Handler hideHandler; // ADFA-4887: nav-bar auto-hide (cleared in onDestroy)
Expand Down Expand Up @@ -223,6 +224,8 @@ public void onPageFinished(WebView view, String url) {

// ADFA-4879: arm/disarm in-app FQR maps depending on whether this is /maps/.
if (fqr != null) fqr.onPageFinished(url);
// ADFA-5004: arm/disarm in-app ZIM manager depending on whether this is /kiwix/.
if (kiwixMgr != null) kiwixMgr.onPageFinished(url);
}

@Override
Expand Down Expand Up @@ -331,6 +334,10 @@ public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
fqr = new org.iiab.controller.redesign.FqrController(this, webView);
fqr.prepareForUrl(finalTargetUrl);

// ADFA-5004: ZIM manager lives in this same shared WebView but activates only on /kiwix/
// (gated in KiwixManageController#onPageFinished).
kiwixMgr = new org.iiab.controller.redesign.KiwixManageController(this, webView);

// Native architecture: content is served locally; load it directly.
webView.loadUrl(finalTargetUrl);
}
Expand Down Expand Up @@ -414,6 +421,7 @@ protected void onDestroy() {
// ADFA-4879: stop FQR polling + drop its overlay/dialog so we don't leak the activity.
// The durable server job (if any) keeps running and shows up on the next /maps/ reload.
if (fqr != null) fqr.detach();
if (kiwixMgr != null) kiwixMgr.detach(); // ADFA-5004
if (hideHandler != null && hideRunnable != null) hideHandler.removeCallbacks(hideRunnable); // ADFA-4887
super.onDestroy();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* ============================================================================
* Name : KiwixClient.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : ADFA-5004. App-side client for the dashboard's Kiwix ZIM-management REST endpoints:
* GET /api/kiwix/library -> installed ZIMs [{ name, bytes, mtime }, ...]
* POST /api/kiwix/delete {name} -> { ok } (unlink + rebuild the Kiwix index)
* Mirrors BooksClient/MapsRegionClient: short, stateless calls off the main thread with
* results posted back to the UI. Only reachable on-device (localhost); the box denies
* /api to remote clients. The ZIM download itself stays a durable job (KiwixDownload).
* ============================================================================
*/
package org.iiab.controller.redesign;

import android.os.Handler;
import android.os.Looper;

import org.iiab.controller.config.BoxEndpoints;
import org.iiab.controller.util.AppExecutors;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public final class KiwixClient {
private KiwixClient() {}

private static final String BASE = BoxEndpoints.API + "/kiwix";
private static final Handler MAIN = new Handler(Looper.getMainLooper());

public interface ArrayCb { void onOk(JSONArray rows); void onErr(String message); }
/** {@code deferred} = the file was removed but the reindex was skipped (a download is in
* progress); kiwix-serve reflects it after that download's own reindex. */
public interface OkCb { void onOk(boolean deferred); void onErr(String message); }

/** The ZIMs currently installed on the box (rows: { name, bytes, mtime }), newest first. */
public static void library(ArrayCb cb) {
AppExecutors.get().io().execute(() -> {
try {
JSONArray a = new JSONArray(httpGet(BASE + "/library"));
MAIN.post(() -> cb.onOk(a));
} catch (Exception e) {
MAIN.post(() -> cb.onErr("couldn't reach the content service"));
}
});
}

/** Delete one installed ZIM by its file name (e.g. "wikipedia_en_all_maxi_2024-01.zim").
* The server removes the file and rebuilds the Kiwix index; this returns when that completes. */
public static void delete(String name, OkCb cb) {
AppExecutors.get().io().execute(() -> {
try {
String resp = httpPostJson(BASE + "/delete", new JSONObject().put("name", name));
final boolean deferred = !resp.isEmpty() && new JSONObject(resp).optBoolean("deferred", false);
MAIN.post(() -> cb.onOk(deferred));
} catch (Exception e) {
MAIN.post(() -> cb.onErr("delete failed"));
}
});
}

private static String httpGet(String urlStr) throws Exception {
HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection();
try {
c.setUseCaches(false);
c.setConnectTimeout(5000);
c.setReadTimeout(8000);
c.setRequestProperty("Accept", "application/json");
int code = c.getResponseCode();
String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream());
if (code < 200 || code >= 400) throw new Exception("HTTP " + code + ": " + text);
return text.isEmpty() ? "[]" : text;
} finally {
c.disconnect();
}
}

/** POST a JSON body and return the response text (for reading fields like {@code deferred}). */
private static String httpPostJson(String urlStr, JSONObject body) throws Exception {
HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection();
try {
c.setUseCaches(false);
c.setConnectTimeout(5000);
// The delete rebuilds the Kiwix index server-side; give it room.
c.setReadTimeout(60000);
c.setRequestMethod("POST");
c.setRequestProperty("Accept", "application/json");
c.setDoOutput(true);
c.setRequestProperty("Content-Type", "application/json");
byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8);
try (OutputStream os = c.getOutputStream()) { os.write(payload); }
int code = c.getResponseCode();
String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream());
if (code < 200 || code >= 400) throw new Exception("HTTP " + code + ": " + text);
return text;
} finally {
c.disconnect();
}
}

private static String readAll(InputStream is) throws Exception {
if (is == null) return "";
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
is.close();
return buf.toString(StandardCharsets.UTF_8.name());
}
}
Loading