// ControlServer.java package com.stock.pignon; import android.content.Context; import android.os.Environment; import android.util.Log; import org.json.JSONObject; import fi.iki.elonen.NanoHTTPD; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.util.Scanner; /** * Create a web server to remote management of app assets. * Inherit what is needed to build a web server from NanoHTTPD */ public class ControlServer extends NanoHTTPD { private final Context context; // Assets folders private final File storageRoot = Environment.getExternalStorageDirectory(); private final File baseDir = new File(storageRoot, Config.EXTERNAL_DIR_NAME); private final File imagesDir = new File(baseDir, Config.IMAGES_SUBDIR_NAME); public ControlServer(Context context, int port) { super(port); this.context = context; } /** * Main method which manage requests */ @Override public Response serve(IHTTPSession session) { // Get requested address String uri = session.getUri(); if (uri.startsWith("/img_view/")) { return serveImage(uri.replace("/img_view/", "")); } if (session.getMethod() == Method.GET) { switch (uri) { // Soon deprecated with online editor case "/download_input": return downloadFile(Config.INPUT_JSON_NAME); // Soon deprecated with online editor case "/output_json": return viewFile(Config.OUTPUT_JSON_NAME); case "/download_output_json": return downloadFile(Config.OUTPUT_JSON_NAME); case "/output_csv": return viewFile(Config.OUTPUT_CSV_NAME); case "/download_output_csv": return downloadFile(Config.OUTPUT_CSV_NAME); case "/edit": return newFixedLengthResponse(fillEditor()); case "/": return newFixedLengthResponse(getHtml("index.html")); } } if (session.getMethod() == Method.POST) { switch (uri) { // Soon deprecated with online editor case "/upload_json": return handleJsonUpload(session); // Soon deprecated with online editor case "/upload_images": return handleImagesUpload(session); case "/save_editor": return handleSaveEditor(session); } } return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Page non trouvée"); } /** * Read from asset and return html file */ private String getHtml(String file) { try { InputStream is = context.getAssets().open(file); // Scanner html file from the beginning with "\\A" Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); String html = s.hasNext() ? s.next() : ""; is.close(); return html; } catch (IOException e) { return "❌ Erreur de chargement de la page"; } } /** * Read from asset and return html file */ private String fillEditor() { // Get data from assets DataLoader.loadData(); // Create an html string to fill the template StringBuilder html = new StringBuilder(); // Save categories order List orderList = new ArrayList<>(); // Globals String globalId = "global"; Category globalCat = new Category("global", DataLoader.getGlobalItems()); html.append(renderCategorySection(globalId, globalCat)); orderList.add(globalId); // Browser categories int index = 0; for (Category cat : DataLoader.getCategories()) { // cat_0, cat_1... String techId = "cat_" + index; html.append(renderCategorySection(techId, cat)); orderList.add(techId); index++; } String orderField = ""; return getHtml("editor.html").replace("{{GENERATED_CONTENT}}", html + orderField); } /** * Generate HTML section for each category */ private String renderCategorySection(String techId, Category cat) { String displayName = cat.getName(); List items = cat.getItems(); StringBuilder sb = new StringBuilder(); sb.append("
"); sb.append("

"); if ("global".equals(displayName)) { sb.append("Articles globaux"); sb.append(""); techId = "global"; } else { // Nom sb.append(""); // Couleur de Fond (on utilise cat.getBgColor()) sb.append("
"); sb.append("
Fond
"); sb.append(""); sb.append("
"); // Couleur de Texte (on utilise cat.getTextColor()) sb.append("
"); sb.append("
Texte
"); sb.append(""); sb.append("
"); // Bouton supprimer sb.append(" "); } sb.append("

"); // Build table sb.append(""); sb.append(""); int rowIdx = 0; for (Item item : items) { sb.append(renderItemRow(techId, item, rowIdx++)); } sb.append("
ImageNomPrix MinPrix Max
"); // Button to add item in this category sb.append(""); sb.append("
"); return sb.toString(); } /** * Generate HTML row for each item */ private String renderItemRow(String techId, Item item, int idx) { String prefix = "item|" + techId + "|" + idx; return "" + "" + "
" + "" + "" + "" + "" + // Retro JS compatibility "" + ""; } private Response serveImage(String filename) { File imgJpg = new File(imagesDir, filename + ".jpg"); File imgPng = new File(imagesDir, filename + ".png"); File file = imgJpg.exists() ? imgJpg : (imgPng.exists() ? imgPng : null); if (file == null || !file.exists()) return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "No image"); try { Response res = newChunkedResponse(Response.Status.OK, "image/jpeg", new FileInputStream(file)); // Avoid cache if user change image res.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.addHeader("Pragma", "no-cache"); res.addHeader("Expires", "0"); return res; } catch (IOException e) { return newFixedLengthResponse("Error"); } } /** * Receive JSON and update cache and storage catalog */ private Response handleSaveEditor(IHTTPSession session) { try { // NanoHTTPD read request Map files = new HashMap<>(); session.parseBody(files); String jsonStr = files.get("postData"); if (jsonStr == null || jsonStr.isEmpty()) { return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Données vides"); } JSONObject fullJson = new JSONObject(jsonStr); // Get items JSONObject allFields = fullJson.getJSONObject("items"); // Get categories order String[] orderedIds = fullJson.getString("cat_order_list").split(","); List finalData = new ArrayList<>(); // For each category for (String techId : orderedIds) { String catNameKey = "cat|" + techId + "|name"; // Does it exist if (!allFields.has(catNameKey)) continue; String name = allFields.getString(catNameKey); List itemsInCategory = new ArrayList<>(); // Browse and create items int i = 0; while (true) { String itemBase = "item|" + techId + "|" + i + "|"; // On vérifie si l'item suivant existe (via son champ name) if (!allFields.has(itemBase + "name")) break; // Manage image String imgVal = allFields.optString(itemBase + "img", ""); if (imgVal.startsWith("data:image")) { try { String newImgName = "img_" + System.currentTimeMillis() + "_" + i; String base64Data = imgVal.split(",")[1]; byte[] decoded = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT); File imageFile = new File(imagesDir, newImgName + ".jpg"); try (FileOutputStream fos = new FileOutputStream(imageFile)) { fos.write(decoded); } imgVal = newImgName; // Replace base64 with name of created jpg file } catch (Exception e) { Log.e("ControlServer", "Img Error", e); } } itemsInCategory.add(new Item( // Mandatory allFields.getString(itemBase + "name"), // Not mandatory imgVal, parseSafely(allFields.optString(itemBase + "min", "0")), parseSafely(allFields.optString(itemBase + "max", "0")) )); i++; } // Create category with items and colors Category cat = new Category(name, itemsInCategory); cat.setBgColor(allFields.optString("cat|" + techId + "|bgColor", "#0049AF")); cat.setTextColor(allFields.optString("cat|" + techId + "|textColor", "#FFFFFF")); finalData.add(cat); } DataLoader.saveData(finalData); return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK"); } catch (Exception e) { Log.e("ControlServer", "Erreur save", e); return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Erreur"); } } private int parseSafely(String val) { if (val == null) return 0; try { return Integer.parseInt(val.trim()); } catch (Exception e) { return 0; } } // Soon deprecated with online editor /** * Manage JSON sent by remote. */ private Response handleJsonUpload(IHTTPSession session) { Map tmpFiles = new HashMap<>(); try { // NanoHTTPD stores loaded file in temp file session.parseBody(tmpFiles); // tmpFiles map stores file name and path to tmp folder String tmpPath = tmpFiles.get("json_file"); if (tmpPath == null) return newFixedLengthResponse("❌ Aucun fichier reçu."); File src = new File(tmpPath); File dest = new File(baseDir, Config.INPUT_JSON_NAME); copyFile(src, dest); return newFixedLengthResponse("✅ Catalogue mis à jour ! Retour"); } catch (Exception e) { return newFixedLengthResponse("❌ Erreur JSON : " + e.getMessage()); } } // Soon deprecated with online editor /** * Manage multiple images sent by remote. */ private Response handleImagesUpload(IHTTPSession session) { Map tmpFiles = new HashMap<>(); try { session.parseBody(tmpFiles); if (!imagesDir.exists() && !imagesDir.mkdirs()) { return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "❌ Erreur : Impossible de créer le dossier des images sur la tablette."); } int count = 0; // Browse temp files created by NanoHTTPD for (Map.Entry entry : tmpFiles.entrySet()) { // Manage multiple files uppload if (entry.getKey().startsWith("images")) { String tmpPath = entry.getValue(); // Get file name List params = session.getParameters().get(entry.getKey()); if (params != null && !params.isEmpty()) { // Clean file name to remove path, only keep image.jpg or image.png String originalName = new File(params.get(0)).getName(); File src = new File(tmpPath); File dest = new File(imagesDir, originalName); copyFile(src, dest); count++; } } } return newFixedLengthResponse("✅ " + count + " images enregistrées ! Retour"); } catch (Exception e) { return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "❌ Erreur serveur : " + e.getMessage()); } } // Soon deprecated with online editor /** * Utility method to copy file byte per byte. * Not always possible to simply move a file on Android. */ private void copyFile(File src, File dst) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } } /** * Prepare file and offer as download */ private Response downloadFile(String filename) { File file = new File(baseDir, filename); if (!file.exists()) { return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé."); } try { InputStream is = new FileInputStream(file); // Force download Response res = newChunkedResponse(Response.Status.OK, "application/octet-stream", is); res.addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); return res; } catch (IOException e) { return newFixedLengthResponse("❌ Erreur de lecture."); } } /** * Prepare file and print it in browser */ private Response viewFile(String filename) { File file = new File(baseDir, filename); if (!file.exists()) { return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé."); } try { InputStream is = new FileInputStream(file); // Force Mime type to print file inside browser String mimeType = "text/plain"; Response res = newChunkedResponse(Response.Status.OK, mimeType + "; charset=utf-8", is); res.addHeader("Content-Disposition", "inline"); return res; } catch (IOException e) { return newFixedLengthResponse("❌ Erreur de lecture."); } } }