Copy this entire file into a new chat with the Android Studio AI assistant (or any AI), or hand it to an Android developer. It is a complete, self-contained specification. Build the app exactly as described, in Java (no Kotlin).
You are an expert Android engineer. Build, from scratch, a small offline Android app (Java, Android Studio) that scans a QR code produced by a Windows desktop app ("WinLogosheet") and displays the substation hourly logsheet as a grid, laid out exactly like the PC.
There is no network, no backend, no accounts. The QR is scanned by the camera, the text is parsed locally, and the grid is drawn on screen. That's the whole app.
- A title / top bar with a small "Scan QR" button and a date label.
- When Scan QR is tapped → open the camera → scan the QR → parse the text → render the grid below.
- If nothing scanned yet, show a friendly empty state ("Tap Scan QR to load a logsheet").
- The grid is wider and taller than the phone, so it must scroll horizontally AND vertically.
- A malformed / non-LS1 QR shows a readable error message, never a crash.
The desktop encodes the whole logsheet as one plain-text string, kept inside the QR
"alphanumeric" character set on purpose (0-9 A-Z space $ % * + - . / :). Format:
LS1 <YYYYMMDD>*<row>*<row>*<row>...
- Header: the literal
LS1, a single space, then the sheet date asYYYYMMDD. - Rows are separated by
*. - Each row is:
[-]HH:c1.c2.c3. ... .c24HH= two-digit hour, values08..24then01..07(see §2 for the canonical order).- A leading
-(beforeHH) means this hour is marked "skip" on the PC (excluded from the printout). Show it, but render it struck-through / faded. - After the
:there are always exactly 24 cells, separated by.. - A cell is an integer string (e.g.
132,44) or empty (no value). Empty cells still occupy their position, so you see adjacent dots:12..7means[12] [empty] [7].
- Not every hour is always present. The QR contains 1..24 rows — only the hours the operator has data for. Render whatever rows arrive, always in the canonical order of §2.
LS1 20260430*08:132.250.80.10.133.205.62.8.33.410.92.12.33.380.85.11.32.300.70.9.45.30.20.15*09:131.240..9.....33.400.90.12.........44..19.14*-10:132.250.80.10.133.205.62.8.33.410.92.12.33.380.85.11.32.300.70.9.45.30.20.15
Decodes to: date 2026-04-30, three rows — hour 08 (full), hour 09 (several empty cells), hour 10 (marked skip). Each row expands to exactly 24 cells.
Tip while developing: add a hidden "paste test text" path, or hard-code the string above, so you can build the grid before wiring the camera.
There are 24 data columns per hour, grouped into 6 sections. The cell index is 0-based
into the 24 cells of a row (cell 0 = the first cell after HH:).
| cell idx | Group | Metric | Notes |
|---|---|---|---|
| 0 | Yarmja | KV | 132 kV bus |
| 1 | Yarmja | A | |
| 2 | Yarmja | MW | |
| 3 | Yarmja | MV (=MVAR) | |
| 4 | Qayra | KV | 132 kV bus |
| 5 | Qayra | A | |
| 6 | Qayra | MW | |
| 7 | Qayra | MV (=MVAR) | |
| 8 | T1 | KV | 33 kV bus |
| 9 | T1 | A | |
| 10 | T1 | MW | |
| 11 | T1 | MV (=MVAR) | |
| 12 | T2 | KV | 33 kV bus |
| 13 | T2 | A | |
| 14 | T2 | MW | |
| 15 | T2 | MV (=MVAR) | |
| 16 | T3 | KV | 33 kV bus |
| 17 | T3 | A | |
| 18 | T3 | MW | |
| 19 | T3 | MV (=MVAR) | |
| 20 | Domez | WM | feeder (single value) |
| 21 | Summer | WM | feeder (single value) |
| 22 | Salam 1 | WM | feeder (single value) |
| 23 | Salam 2 | WM | feeder (single value) |
Header layout (two rows): the 5 transformer groups (Yarmja, Qayra, T1, T2, T3) each span 4 sub-columns (KV / A / MW / MV). The 4 feeders (Domez, Summer, Salam 1, Salam 2) are 1 column each, sub-label WM. Left-most fixed column is the hour (Hrs).
│ Yarmja │ Qayra │ T1 │ ... │ Domez │Summer │Salam1 │Salam2 │
Hrs │ KV │ A │ MW │ MV │ KV │ A │ MW │ MV │ KV │ A │ MW │ MV │ ... │ WM │ WM │ WM │ WM │
Hour order (canonical): 8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24, 1,2,3,4,5,6,7.
Always display rows in this order regardless of the order they appear in the QR.
Column group background tints:
| Group | Cell background | Group-header (darker) |
|---|---|---|
| Yarmja | 0xFFE6E6FA |
0xFFC3C3D7 |
| Qayra | 0xFFE6FFE6 |
0xFFC3DCC3 |
| T1 | 0xFFFFF0F5 |
0xFFDCCDD2 |
| T2 | 0xFFF0FFF0 |
0xFFCDDCCD |
| T3 | 0xFFF0F8FF |
0xFFCDD5DC |
| Feeders | 0xFFFFF5EE |
0xFFDCD2CB |
| Hrs col | header 0xFFD7D7D7 / cells white |
Grid lines: light gray 0xFF808080, 1px. Text: black, centered in each cell.
(The group-header color is simply the cell color with 35 subtracted from each RGB channel.)
The PC tints individual cells when a reading looks wrong. A cell's validation color overrides its group tint. Values are treated as numbers.
Colors: Missing 0xFFFF6E6E (red) · Suspect 0xFFFFF564 (yellow) ·
Overload / High-MVAR 0xFFFFAA32 (orange).
Rules (apply per cell; the tolerance for the transformer/feeder comparisons is 1.0):
- Missing (red): cell is empty but its hour-row has at least one other non-empty cell.
- If a non-empty cell is not a valid number → no color.
- Feeder columns (cell idx 20–23, WM):
value < 0orvalue > 500→ yellow.- Else if
|value| > |parent transformer MW| + 1.0→ orange. Parent MW cell: Domez(20)→T1 MW(10), Summer(21)→T2 MW(14), Salam 1(22)→T2 MW(14), Salam 2(23)→T3 MW(18). (If the parent MW cell is empty/non-numeric, skip this check.)
- Transformer columns — sub-type =
cellIdx % 4:0=KV, 1=A, 2=MW, 3=MV.132 kVgroups = cell idx 0–7 (Yarmja, Qayra).33 kVgroups = cell idx 8–19 (T1,T2,T3).- KV (subtype 0): 132 kV group outside 118..145 → yellow; 33 kV group outside 28..38 → yellow.
- A (subtype 1):
value < 0→ yellow. - MW (subtype 2), 33 kV only:
|value| > 150→ yellow (checked first, takes priority).- else if
sum(|child feeder WM|) > |value| + 1.0→ orange (load-balance). Children: T1 MW(10)→{Domez 20}; T2 MW(14)→{Summer 21, Salam 1 22}; T3 MW(18)→{Salam 2 23}. (Only feeders that are present/numeric count; if none present, skip.) - 132 kV MW is intentionally not validated.
- MV / MVAR (subtype 3), 33 kV only: if
|value| > 100→ look at the same group's KV (cell idx − 3). If that KV is in 28..38 → orange (High-MVAR), otherwise → yellow.
If you want to ship faster, you can skip §4 at first and just use the group tints from §3 — the app is still fully usable. But §6.3 gives you the exact code, so prefer to include it.
- Java, Android Views (XML layout + programmatic table), AppCompatActivity.
- min SDK 24, compile/target SDK 34 (or latest stable).
- Scanning: ZXing Android Embedded (
com.journeyapps:zxing-android-embedded) — fully offline, launches its own camera scan screen, requests the camera permission for you, and returns the decoded string. - No other third-party dependencies required.
android {
namespace 'com.example.winlogosheetviewer'
compileSdk 34
defaultConfig {
applicationId "com.example.winlogosheetviewer"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures { viewBinding false }
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.winlogosheetviewer">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:allowBackup="true"
android:label="Logsheet Viewer"
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>ZXing's own CaptureActivity is merged in automatically by the library — you do not declare it.
Package everything under com.example.winlogosheetviewer.
package com.example.winlogosheetviewer;
import java.util.List;
public class LogsheetRow {
public final int hour;
public final boolean skipped;
public final List<String> cells; // always size 24; "" = empty cell
public LogsheetRow(int hour, boolean skipped, List<String> cells) {
this.hour = hour;
this.skipped = skipped;
this.cells = cells;
}
}package com.example.winlogosheetviewer;
import java.util.List;
public class Logsheet {
public final String dateText; // "yyyy-MM-dd" or null
public final List<LogsheetRow> rows; // in canonical hour order
public Logsheet(String dateText, List<LogsheetRow> rows) {
this.dateText = dateText;
this.rows = rows;
}
}package com.example.winlogosheetviewer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Parses the "LS1 ..." offline payload produced by WinLogosheet (LogsheetQr.cs). */
public final class LogsheetQrParser {
private static final int[] HOUR_ORDER = {
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
1, 2, 3, 4, 5, 6, 7
};
private LogsheetQrParser() {}
public static Logsheet parse(String payload) {
if (payload == null) throw new IllegalArgumentException("Empty payload");
String text = payload.trim();
// limit -1 keeps trailing empty fields (empty cells / trailing rows)
String[] parts = text.split("\\*", -1);
if (parts.length == 0) throw new IllegalArgumentException("Empty payload");
String[] header = parts[0].trim().split(" ");
if (header.length < 2 || !header[0].equals("LS1"))
throw new IllegalArgumentException("Not an LS1 payload");
String date = formatDate(header[1]);
List<LogsheetRow> rows = new ArrayList<>();
for (int p = 1; p < parts.length; p++) {
String s = parts[p].trim();
if (s.isEmpty()) continue;
boolean skipped = s.startsWith("-");
String body = skipped ? s.substring(1) : s;
int colon = body.indexOf(':');
if (colon <= 0) throw new IllegalArgumentException("Malformed row: " + parts[p]);
Integer hour = tryParseInt(body.substring(0, colon));
if (hour == null) throw new IllegalArgumentException("Bad hour in: " + parts[p]);
String[] raw = body.substring(colon + 1).split("\\.", -1); // keep empties
List<String> cells = new ArrayList<>(24);
for (int i = 0; i < 24; i++)
cells.add(digitsOnly(i < raw.length ? raw[i] : ""));
rows.add(new LogsheetRow(hour, skipped, cells));
}
Collections.sort(rows, (a, b) ->
Integer.compare(orderIndex(a.hour), orderIndex(b.hour)));
return new Logsheet(date, rows);
}
private static int orderIndex(int hour) {
for (int i = 0; i < HOUR_ORDER.length; i++)
if (HOUR_ORDER[i] == hour) return i;
return Integer.MAX_VALUE; // unknown hours sort to the end
}
private static String formatDate(String yyyymmdd) {
if (yyyymmdd != null && yyyymmdd.length() == 8) {
return yyyymmdd.substring(0, 4) + "-" +
yyyymmdd.substring(4, 6) + "-" +
yyyymmdd.substring(6, 8);
}
return yyyymmdd;
}
private static String digitsOnly(String raw) {
if (raw == null || raw.isEmpty()) return "";
StringBuilder sb = new StringBuilder(raw.length());
for (int i = 0; i < raw.length(); i++) {
char c = raw.charAt(i);
if (c >= '0' && c <= '9') sb.append(c);
}
return sb.toString();
}
private static Integer tryParseInt(String s) {
try { return Integer.parseInt(s.trim()); }
catch (NumberFormatException e) { return null; }
}
}package com.example.winlogosheetviewer;
/** Mirrors GetValidationColor / CheckFeederAgainstTransformer /
* CheckTransformerLoadBalance from the PC's Form1.cs.
* Returns an ARGB color for the cell, or 0 for "no override" (use group tint). */
public final class LogsheetValidator {
public static final int MISSING = 0xFFFF6E6E; // red
public static final int SUSPECT = 0xFFFFF564; // yellow
public static final int OVERLOAD = 0xFFFFAA32; // orange
private static final double TOL = 1.0;
private LogsheetValidator() {}
/** cellIndex 0..23. Returns 0 when there is no validation override. */
public static int cellColor(int cellIndex, LogsheetRow row) {
String value = row.cells.get(cellIndex);
boolean empty = value == null || value.trim().isEmpty();
// 1. Missing value in a row that has other data
if (empty) return rowHasAnyData(row) ? MISSING : 0;
Double parsed = tryParseDouble(value);
if (parsed == null) return 0;
double val = parsed;
// Feeders (WM): cell idx 20..23
if (cellIndex >= 20) {
if (val < 0 || val > 500) return SUSPECT;
int parentMw = feederParentMwIndex(cellIndex);
Double pmw = cellNum(row, parentMw);
if (pmw != null && Math.abs(val) > Math.abs(pmw) + TOL) return OVERLOAD;
return 0;
}
int subType = cellIndex % 4; // 0 KV, 1 A, 2 MW, 3 MV
boolean is132 = cellIndex <= 7; // Yarmja + Qayra
boolean is33 = cellIndex >= 8 && cellIndex <= 19; // T1, T2, T3
switch (subType) {
case 0: // KV
if (is132 && (val < 118 || val > 145)) return SUSPECT;
if (is33 && (val < 28 || val > 38)) return SUSPECT;
break;
case 1: // A
if (val < 0) return SUSPECT;
break;
case 2: // MW (33 kV only)
if (is33 && Math.abs(val) > 150) return SUSPECT;
if (is33) {
int bal = transformerBalance(cellIndex, val, row);
if (bal != 0) return bal;
}
break;
case 3: // MV / MVAR (33 kV only)
if (is33 && Math.abs(val) > 100) {
Double kv = cellNum(row, cellIndex - 3); // group KV cell
boolean kvOk = kv != null && kv >= 28 && kv <= 38;
return kvOk ? OVERLOAD : SUSPECT;
}
break;
}
return 0;
}
private static boolean rowHasAnyData(LogsheetRow row) {
for (String c : row.cells)
if (c != null && !c.trim().isEmpty()) return true;
return false;
}
// Domez→T1 MW(10), Summer→T2 MW(14), Salam1→T2 MW(14), Salam2→T3 MW(18)
private static int feederParentMwIndex(int feederIdx) {
switch (feederIdx) {
case 20: return 10; // Domez → T1
case 21: return 14; // Summer → T2
case 22: return 14; // Salam1 → T2
case 23: return 18; // Salam2 → T3
default: return -1;
}
}
// MW cell vs the sum of its child feeders (orange if feeders exceed transformer).
private static int transformerBalance(int mwIdx, double xfmrMw, LogsheetRow row) {
double sum = 0; boolean any = false;
int[] children;
switch (mwIdx) {
case 10: children = new int[]{20}; break; // T1 ← Domez
case 14: children = new int[]{21, 22}; break; // T2 ← Summer, Salam1
case 18: children = new int[]{23}; break; // T3 ← Salam2
default: return 0;
}
for (int c : children) {
Double v = cellNum(row, c);
if (v != null) { sum += Math.abs(v); any = true; }
}
if (!any) return 0;
return sum > Math.abs(xfmrMw) + TOL ? OVERLOAD : 0;
}
private static Double cellNum(LogsheetRow row, int idx) {
if (idx < 0 || idx >= row.cells.size()) return null;
return tryParseDouble(row.cells.get(idx));
}
private static Double tryParseDouble(String s) {
if (s == null || s.trim().isEmpty()) return null;
try { return Double.parseDouble(s.trim()); }
catch (NumberFormatException e) { return null; }
}
}Builds the table into a vertical LinearLayout (the grid container). Each cell is a
fixed-width TextView, so columns line up exactly (the "coordinates" requirement). Group
name cells span 4 data columns by being 4× as wide.
package com.example.winlogosheetviewer;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.util.TypedValue;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.Locale;
public final class GridRenderer {
private static final int HRS_W_DP = 48;
private static final int DATA_W_DP = 56;
private static final int ROW_H_DP = 30;
private static final int GRID_LINE = 0xFF808080;
private static final int HRS_HDR_BG = 0xFFD7D7D7;
static final String[] TRANSFORMERS = {"Yarmja", "Qayra", "T1", "T2", "T3"};
static final String[] FEEDERS = {"Domez", "Summer", "Salam 1", "Salam 2"};
private GridRenderer() {}
/** container = a vertical LinearLayout inside the 2-axis scroll (see §6.5 layout). */
public static void render(LinearLayout container, Logsheet sheet) {
Context ctx = container.getContext();
container.removeAllViews();
container.setOrientation(LinearLayout.VERTICAL);
int hrsW = dp(ctx, HRS_W_DP);
int dataW = dp(ctx, DATA_W_DP);
int rowH = dp(ctx, ROW_H_DP);
// Row 1 — group names (transformers span 4 data columns, feeders span 1)
LinearLayout r1 = row(ctx);
r1.addView(cell(ctx, "Hrs", hrsW, rowH, HRS_HDR_BG, true, false));
for (String g : TRANSFORMERS)
r1.addView(cell(ctx, g, dataW * 4, rowH, groupHeaderColor(g), true, false));
for (String f : FEEDERS)
r1.addView(cell(ctx, f, dataW, rowH, groupHeaderColor("Feeders"), true, false));
container.addView(r1);
// Row 2 — metric sub-headers
LinearLayout r2 = row(ctx);
r2.addView(cell(ctx, "", hrsW, rowH, HRS_HDR_BG, false, false));
for (int i = 0; i < 24; i++)
r2.addView(cell(ctx, metricLabel(i), dataW, rowH, groupLightColor(i), true, false));
container.addView(r2);
// Data rows (already in canonical hour order)
for (LogsheetRow lr : sheet.rows) {
LinearLayout dr = row(ctx);
String hourText = String.format(Locale.US, "%02d", lr.hour);
dr.addView(cell(ctx, hourText, hrsW, rowH, 0xFFFFFFFF, true, lr.skipped));
for (int i = 0; i < 24; i++) {
String value = lr.cells.get(i);
int vc = LogsheetValidator.cellColor(i, lr);
int bg = vc != 0 ? vc : groupLightColor(i);
dr.addView(cell(ctx, value, dataW, rowH, bg, false, false));
}
container.addView(dr);
}
}
private static LinearLayout row(Context ctx) {
LinearLayout ll = new LinearLayout(ctx);
ll.setOrientation(LinearLayout.HORIZONTAL);
return ll;
}
private static TextView cell(Context ctx, String text, int w, int h, int bg,
boolean bold, boolean strike) {
TextView tv = new TextView(ctx);
tv.setText(text == null ? "" : text);
tv.setLayoutParams(new LinearLayout.LayoutParams(w, h));
tv.setGravity(Gravity.CENTER);
tv.setTextColor(0xFF000000);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
tv.setSingleLine(true);
if (bold) tv.setTypeface(Typeface.DEFAULT_BOLD);
if (strike) tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
GradientDrawable border = new GradientDrawable();
border.setColor(bg);
border.setStroke(1, GRID_LINE); // 1px grid line
tv.setBackground(border);
return tv;
}
private static String metricLabel(int cellIndex) {
if (cellIndex >= 20) return "WM";
switch (cellIndex % 4) {
case 0: return "KV";
case 1: return "A";
case 2: return "MW";
default: return "MV";
}
}
private static int groupLightColor(int cellIndex) {
if (cellIndex <= 3) return 0xFFE6E6FA; // Yarmja
if (cellIndex <= 7) return 0xFFE6FFE6; // Qayra
if (cellIndex <= 11) return 0xFFFFF0F5; // T1
if (cellIndex <= 15) return 0xFFF0FFF0; // T2
if (cellIndex <= 19) return 0xFFF0F8FF; // T3
return 0xFFFFF5EE; // feeders
}
private static int groupHeaderColor(String group) {
switch (group) {
case "Yarmja": return 0xFFC3C3D7;
case "Qayra": return 0xFFC3DCC3;
case "T1": return 0xFFDCCDD2;
case "T2": return 0xFFCDDCCD;
case "T3": return 0xFFCDD5DC;
default: return 0xFFDCD2CB; // feeders
}
}
private static int dp(Context ctx, int value) {
return (int) (value * ctx.getResources().getDisplayMetrics().density + 0.5f);
}
}A vertical page: the top bar, an empty-state / error message, and a 2-axis scroll
(HorizontalScrollView wrapping a ScrollView wrapping the grid container).
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/btnScan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scan QR" />
<TextView
android:id="@+id/dateLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Tap “Scan QR” to load a logsheet." />
<HorizontalScrollView
android:id="@+id/hScroll"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="12dp"
android:visibility="gone">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</HorizontalScrollView>
</LinearLayout>Wires the button to ZXing's ScanContract (via the AndroidX ActivityResult API), parses the
result, and renders the grid. On any parse error it shows a readable message instead.
package com.example.winlogosheetviewer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AppCompatActivity;
import com.journeyapps.barcodescanner.ScanContract;
import com.journeyapps.barcodescanner.ScanIntentResult;
import com.journeyapps.barcodescanner.ScanOptions;
public class MainActivity extends AppCompatActivity {
private LinearLayout grid;
private View hScroll;
private TextView dateLabel;
private TextView message;
private final ActivityResultLauncher<ScanOptions> scanLauncher =
registerForActivityResult(new ScanContract(), this::onScanResult);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
grid = findViewById(R.id.grid);
hScroll = findViewById(R.id.hScroll);
dateLabel = findViewById(R.id.dateLabel);
message = findViewById(R.id.message);
Button scan = findViewById(R.id.btnScan);
scan.setOnClickListener(v -> startScan());
}
private void startScan() {
ScanOptions options = new ScanOptions();
options.setDesiredBarcodeFormats(ScanOptions.QR_CODE);
options.setPrompt("Point at the WinLogosheet QR");
options.setBeepEnabled(true);
options.setOrientationLocked(false);
scanLauncher.launch(options);
}
private void onScanResult(ScanIntentResult result) {
String contents = result.getContents();
if (contents == null) return; // user pressed back / cancelled
try {
Logsheet sheet = LogsheetQrParser.parse(contents);
showSheet(sheet);
} catch (Exception ex) {
showError(ex.getMessage() == null ? "Could not read QR" : ex.getMessage());
}
}
private void showSheet(Logsheet sheet) {
dateLabel.setText(sheet.dateText == null ? "" : "Date: " + sheet.dateText);
GridRenderer.render(grid, sheet);
message.setVisibility(View.GONE);
hScroll.setVisibility(View.VISIBLE);
}
private void showError(String msg) {
grid.removeAllViews();
hScroll.setVisibility(View.GONE);
message.setVisibility(View.VISIBLE);
message.setText("⚠ " + msg);
}
}- Fresh install → one screen with a Scan QR button and the empty-state message.
- Tapping Scan QR opens the camera (permission requested on first run) and scans a QR.
- Feeding the test vector from §1.1 shows a grid with Date: 2026-04-30 and rows 08, 09, 10, columns grouped Yarmja/Qayra/T1/T2/T3 + Domez/Summer/Salam 1/Salam 2, sub-columns KV/A/MW/MV (WM for feeders), values in the correct columns, empty cells blank, hour 10 shown struck-through (skipped).
- The grid scrolls horizontally and vertically; columns stay aligned.
- Group background tints match §3; validation tints match §4 (red/yellow/orange override the group tint on the offending cells).
- A malformed / non-LS1 QR shows a readable error, not a crash.
- Missing hours: the QR may carry only some of the 24 hours → show only those rows.
- Skipped hours: leading
-→ render the hour struck-through / faded, but still show its values. - Empty cells: consecutive dots → blank cells; keep column alignment (always 24 cells).
- Large QR: a full sheet is a dense QR (up to version 40, 177×177 modules). Ask the operator to display it large on the PC and hold the phone steady; ZXing handles it.
- Non-digit junk in a cell is stripped by the parser (values are integers only).
The Windows app builds this exact payload in LogsheetQr.cs (BuildPayload) and renders the
QR with the QRCoder library at error-correction level M. The validation colors replicate
GetValidationColor in Form1.cs. If the format ever changes, the header tag bumps from
LS1 to LS2, etc. — check the tag and reject unknown versions gracefully.