From 19dd344610c056133a2cc5128ccf0d06fbe6ab5b Mon Sep 17 00:00:00 2001 From: Und3rf10w Date: Mon, 5 Jan 2026 10:55:21 -0500 Subject: [PATCH 01/13] feat: MVP preparation - fix compilation, add tests, improve UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit brings the OpenPrintTag Writer to MVP-ready state with critical bug fixes, comprehensive testing, and UX improvements. - Fix type errors: lowercase `int?` → `Int?` in OpenPrintTagModel.kt (totalWeight, ActTotalWeight, consumedWeight fields) - Add missing `JsonNode` import in Serializer.kt - Remove duplicate nested onNewIntent() inside onCreate() that shadowed the class-level override in MainActivity - Wire materialClass dropdown to model in GeneratorActivity (was being ignored during serialization) - Add NFC intent filters to AndroidManifest for proper tag discovery: - NDEF_DISCOVERED for application/vnd.openprinttag MIME type - TECH_DISCOVERED with nfc_tech_filter.xml (NfcA, NfcV, Ndef) - Set versionCode=1, versionName="0.1.0" - Add test dependencies (JUnit 4.13.2, AndroidX Test, Espresso) - Create proguard-rules.pro with rules for: - Kotlinx.serialization (keep serializers and companions) - Jackson CBOR (keep ObjectMapper methods) - SnakeYAML and model classes - Update CI workflow to use JDK 21 and add explicit build step - Create adaptive app icons (vector drawables): - ic_launcher_background.xml (teal gradient) - ic_launcher_foreground.xml (3D printer/tag design) - ic_launcher.xml and ic_launcher_round.xml (adaptive configs) - Create comprehensive strings.xml with 40+ externalized strings for all UI text, error messages, toasts, and status indicators - Add mode indicator colors to colors.xml: - mode_read_background (#4CAF50 green) - mode_write_background (#F44336 red) - SerializerTest.kt: 17 tests covering CBOR encoding, NDEF formatting, dual-record generation, and deserialization - OpenPrintTagModelTest.kt: 17 tests for model defaults, field assignments, nested regions, and date serialization - SelectionManagerTest.kt: 15 tests for tag selection logic including implies chains, hints, circular dependencies, and edge cases - Add ProgressBar for visual feedback during NFC operations - Add mode indicator TextView showing current read/write mode with color-coded background (green=read, red=write) - Add try-catch error handling around all NFC operations with user-friendly error messages via Toast and status TextView - Update activity_main.xml layout with new UI components --- .github/workflows/android-ci.yml | 6 +- AGENTS.md | 54 +++++ CLAUDE.md | 68 ++++++ app/build.gradle | 27 +-- app/proguard-rules.pro | 16 ++ app/src/main/AndroidManifest.xml | 16 +- .../org/openprinttag/GeneratorActivity.kt | 160 +------------ .../java/org/openprinttag/MainActivity.kt | 123 ++++++---- .../openprinttag/model/OpenPrintTagModel.kt | 29 +-- .../java/org/openprinttag/model/Serializer.kt | 44 +--- .../res/drawable/ic_launcher_background.xml | 7 + .../res/drawable/ic_launcher_foreground.xml | 7 + .../main/res/layout/activity_generator.xml | 20 +- app/src/main/res/layout/activity_main.xml | 46 +++- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/strings.xml | 68 ++++++ app/src/main/res/xml/nfc_tech_filter.xml | 18 +- app/src/test/java/SerializerUnitTest.kt | 216 +++++++++++++++++- .../org/openprinttag/SelectionManagerTest.kt | 196 ++++++++++++++++ .../model/OpenPrintTagModelTest.kt | 201 ++++++++++++++++ .../org/openprinttag/model/SerializerTest.kt | 28 +++ 23 files changed, 1036 insertions(+), 328 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/test/java/org/openprinttag/SelectionManagerTest.kt create mode 100644 app/src/test/java/org/openprinttag/model/OpenPrintTagModelTest.kt create mode 100644 app/src/test/java/org/openprinttag/model/SerializerTest.kt diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index e5f5472..b237752 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Set up Gradle cache uses: actions/cache@v4 @@ -27,3 +27,5 @@ jobs: ${{ runner.os }}-gradle- - name: Run unit tests run: ./gradlew test --no-daemon + - name: Build debug APK + run: ./gradlew assembleDebug --no-daemon diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a925d6e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This is an Android project written in Kotlin. The repository structure follows the standard Android Gradle plugin conventions: + +- **Source Code**: `app/src/main/java/org/openprinttag/` contains the core application logic, including Activities, Models, and Utilities. +- **Resources**: `app/src/main/res/` holds UI layouts, strings, and other assets. +- **Tests**: `app/src/test/` contains unit tests (e.g., `SerializerUnitTest.kt`). +- **Build Configuration**: Root `build.gradle` and `app/build.gradle` define dependencies and build settings. + +## Build, Test, and Development Commands + +Use the Gradle Wrapper for consistent execution across environments. + +- **Build Project**: + ```bash + ./gradlew assembleDebug + ``` + Compiles the code and generates a debug APK in `app/build/outputs/apk/debug/`. + +- **Run Tests**: + ```bash + ./gradlew test + ``` + Executes unit tests located in `app/src/test/`. + +- **Clean Project**: + ```bash + ./gradlew clean + ``` + Removes the `build/` directory to ensure a fresh build. + +## Coding Style & Naming Conventions + +- **Language**: Kotlin is the primary language. Ensure compatibility with Kotlin 2.2.0 and Java 21. +- **Formatting**: Follow standard Kotlin coding conventions. +- **Naming**: + - **Classes**: PascalCase (e.g., `OpenPrintTagModel`). + - **Functions/Variables**: camelCase (e.g., `generateTag`, `nfcHelper`). + - **Layout Files**: snake_case (e.g., `activity_main.xml`). +- **View Binding**: The project uses View Binding. Avoid `findViewById` where possible. + +## Testing Guidelines + +- **Framework**: JUnit is used for unit testing. +- **Location**: Place unit tests in `app/src/test/java/org/openprinttag/`. +- **Requirement**: Ensure core logic in `model/` and `util/` (like serializers and byte utilities) is covered by tests. + +## Commit & Pull Request Guidelines + +- **Commit Messages**: Use clear, imperative present-tense messages (e.g., "Fix serialization bug", "Add NFC helper"). +- **Pull Requests**: Provide a brief description of the changes. If the PR involves UI changes, include a screenshot or video description if possible. + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8e829a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,68 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +VCOPT (OpenPrintTag Writer) is an Android NFC application for reading, writing, and generating standardized data tags for 3D printing materials (filament spools). It implements the OpenPrintTag specification, storing material metadata (brand, type, temperatures, colors, certifications) on NFC tags using CBOR encoding and NDEF formatting. + +## Build Commands + +```bash +./gradlew assembleDebug # Build debug APK (outputs to app/build/outputs/apk/debug/) +./gradlew assembleRelease # Build release APK +./gradlew test # Run unit tests (in app/src/test/) +./gradlew installDebug # Install to connected device +./gradlew clean # Clean build directory +``` + +## Architecture + +**Package:** `org.openprinttag.app` + +### Core Components + +- **MainActivity** - Entry point handling NFC tag detection, reading, writing, and file import/export. Uses coroutines for async NFC operations with read/write mode toggle. +- **GeneratorActivity** - Material data editor with complex UI for configuring material properties. Implements smart tag selection logic (implies/hints system) and returns generated binary data to MainActivity. +- **NfcHelper** - Abstraction layer for NFC operations supporting both NfcA and NfcV tag technologies. Handles page-based reading (pages 4-129). +- **OpenPrintTagModel** (`model/`) - Data model with nested regions (Meta, Main, Aux, URL). Uses Kotlinx.serialization with custom serializers and `@SerialName` for CBOR integer key mapping. +- **Serializer** (`model/`) - Bidirectional CBOR serialization with NDEF record formatting. MIME type: `application/vnd.openprinttag`. Supports dual-record output (CBOR + URI). + +### Data Configuration + +YAML files in `app/src/main/assets/data/` define: +- `main_fields.yaml` - 50+ field definitions with types, descriptions, units +- `material_type_enum.yaml` - Material types (PLA, PETG, ABS, TPU, etc.) +- `tags_enum.yaml` - Material property tags with implies/hints relationships +- `tag_categories_enum.yaml` - Tag categories with display names and emojis + +### Data Flow + +1. User interacts with MainActivity -> launches GeneratorActivity +2. GeneratorActivity loads YAML configs -> builds dynamic UI +3. User fills form -> clicks Generate +4. Serializer encodes to CBOR -> returns to MainActivity +5. User taps NFC tag -> NfcHelper writes data + +## Coding Conventions + +- **Language**: Kotlin 2.2.0 with Java 21 compatibility +- **Classes**: PascalCase (e.g., `OpenPrintTagModel`) +- **Functions/Variables**: camelCase (e.g., `generateTag`, `nfcHelper`) +- **Layout Files**: snake_case (e.g., `activity_main.xml`) +- **View Binding**: Used throughout - avoid `findViewById` +- **Coroutines**: Dispatchers.IO for NFC/file operations, Dispatchers.Main for UI + +## Testing + +- Unit tests in `app/src/test/java/org/openprinttag/` +- Focus on covering `model/` and `util/` logic (serializers, byte utilities) +- Run with `./gradlew test` + +## Tech Stack + +- Kotlin 2.2.0 / JVM toolchain 21 +- Min SDK 25 / Target SDK 35 +- Kotlinx.serialization + Jackson CBOR +- SnakeYAML for config parsing +- Material Design 3 components diff --git a/app/build.gradle b/app/build.gradle index dc1cf91..9573359 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { compileSdk 35 compileOptions { - sourceCompatibility JavaVersion.VERSION_21 - targetCompatibility JavaVersion.VERSION_21 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 coreLibraryDesugaringEnabled true } @@ -34,17 +34,18 @@ android { applicationId "org.openprinttag.app" minSdk 25 targetSdk 35 + versionCode 1 + versionName "0.1.0" } // 2. Add this block to force Kotlin to match Java (outside the android block) kotlin { - jvmToolchain(21) + jvmToolchain(17) } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.debug } } buildFeatures { @@ -72,25 +73,11 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.18.2' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2' -/* - implementation 'androidx.compose.runtime:runtime:1.10.0' - implementation 'com.google.android.material:material:1.13.0' - implementation 'com.android.identity:identity-jvm:202411.1' - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5" // or latest - implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.0" - implementation 'androidx.appcompat:appcompat:1.7.1' - implementation 'androidx.constraintlayout:constraintlayout:2.2.1' - implementation("org.yaml:snakeyaml:2.5") - implementation("com.github.skydoves:colorpickerview:2.3.0") - implementation("com.github.skydoves:colorpicker-compose:1.1.3") + + // Test dependencies testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.20.1' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.20.1' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0" - implementation("org.jetbrains.kotlin:kotlin-reflect") -*/ } configurations.configureEach { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..50d3d08 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,16 @@ +# Kotlin Serialization +-keepattributes *Annotation*, InnerClasses +-keepclassmembers class kotlinx.serialization.json.** { *** Companion; } +-keep,includedescriptorclasses class org.openprinttag.model.**$$serializer { *; } +-keepclassmembers class org.openprinttag.model.** { *** Companion; } + +# Jackson +-keep class com.fasterxml.jackson.databind.ObjectMapper { public ; } +-keepnames class com.fasterxml.jackson.** { *; } +-dontwarn com.fasterxml.jackson.databind.** + +# SnakeYAML +-keep class org.yaml.snakeyaml.** { *; } + +# Model classes +-keep class org.openprinttag.model.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a5f283..e1b54e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,14 +6,26 @@ + android:theme="@style/Theme.OpenPrintTag"> + + + + + + + + + - + diff --git a/app/src/main/java/org/openprinttag/GeneratorActivity.kt b/app/src/main/java/org/openprinttag/GeneratorActivity.kt index 19168b3..66cbcd8 100644 --- a/app/src/main/java/org/openprinttag/GeneratorActivity.kt +++ b/app/src/main/java/org/openprinttag/GeneratorActivity.kt @@ -73,12 +73,6 @@ class HintSpinnerAdapter( return ContextCompat.getColor(context, typedValue.resourceId) } - /* - override fun isEnabled(position: Int): Boolean { - // Disable the first item (the hint) so it's not clickable - return position != 0 - } */ - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = super.getView(position, convertView, parent) as TextView // Closed state color @@ -204,32 +198,6 @@ class GeneratorActivity : AppCompatActivity() { - class HintSpinnerAdapter( - context: Context, - resource: Int, - objects: List - ) : ArrayAdapter(context, resource, objects) { - - /* - override fun isEnabled(position: Int): Boolean { - // Disable the first item (the hint) so it's not clickable - return position != 0 - } - */ - - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = super.getDropDownView(position, convertView, parent) - val tv = view as TextView - if (position == 0) { - // Set the hint text color to gray - tv.setTextColor(Color.GRAY) - } else { - // Set regular items to black (or your theme color) - tv.setTextColor(Color.BLACK) - } - return view - } - } private fun loadMaterialTypesFromYaml(assetPath: String, fieldName: String): List { val names = mutableListOf() try { @@ -295,74 +263,6 @@ class GeneratorActivity : AppCompatActivity() { } return resultMap } -/* - class OptionsAdapter( - private var options: List, - private val selectionManager: SelectionManager, - private val onSelectionChanged: (Set, List) -> Unit - ) : RecyclerView.Adapter() { - - private var selectedKeys = mutableSetOf() - private val TYPE_HEADER = 0 - private val TYPE_TAG = 1 - - - override fun getItemViewType(position: Int): Int { - return when (options[position]) { - is TagDisplayItem.Header -> TYPE_HEADER - is TagDisplayItem.TagRow -> TYPE_TAG - } - } - // 1. Fixed onCreateViewHolder: Ensure ViewGroup and Int are explicit - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val inflater = LayoutInflater.from(parent.context) - return if (viewType == TYPE_HEADER) { - // Reuse the TextView from your category_select.xml or create a small header layout - val view = inflater.inflate(R.layout.tag_category_header, parent, false) - HeaderViewHolder(view) - } else { - val view = inflater.inflate(R.layout.tag_selectable_option, parent, false) - TagViewHolder(view) - } - } - - // 2. Fixed onBindViewHolder: Matches the ViewHolder type exactly - override fun onBindViewHolder(holder: OptionViewHolder, position: Int) { - holder.bind(options[position]) - } - - override fun getItemCount(): Int = options.size - - - inner class OptionViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val checkbox: CheckBox = view.findViewById(R.id.option_checkbox) - val title: TextView = view.findViewById(R.id.option_title) - val description: TextView = view.findViewById(R.id.option_description) - - fun bind(option: OptionEntry) { - title.text = option.display_name - description.text = option.description - - checkbox.setOnCheckedChangeListener(null) - checkbox.isChecked = selectedKeys.contains(option.key) - - checkbox.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - val update = selectionManager.onOptionSelected(option.key, selectedKeys) - selectedKeys.addAll(update.newSelectedKeys) - onSelectionChanged(selectedKeys, update.suggestions) - notifyDataSetChanged() - } else { - selectedKeys.remove(option.key) - onSelectionChanged(selectedKeys, emptyList()) - } - } - } - } - } - - */ - private fun preFillUI(model: OpenPrintTagModel) { // Basic Fields val main = model.main @@ -623,7 +523,6 @@ class GeneratorActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - //setContentView(R.layout.activity_generator) // Inflate and set the root binding = ActivityGeneratorBinding.inflate(layoutInflater) setContentView(binding.root) @@ -679,6 +578,7 @@ class GeneratorActivity : AppCompatActivity() { model.main.minPrintTemp = getMin.text.toString().toIntOrNull() model.main.maxPrintTemp = getMax.text.toString().toIntOrNull() model.main.materialType = autoCompleteMaterialType.text.toString() + model.main.materialClass = autoCompleteMaterialClass.text.toString().ifBlank { "FFF" } model.main.materialTags = currentSelectedKeys.map { key -> allOptions.find { it.key == key }?.name ?: "" } @@ -697,38 +597,14 @@ class GeneratorActivity : AppCompatActivity() { resultIntent.putExtra("GEN_BIN_DATA", bin) setResult(Activity.RESULT_OK, resultIntent) - /* - val f = File(filesDir, "openprinttag.bin") - FileOutputStream(f).use { it.write(bin) } - Toast.makeText(this, "Generated ${bin.size} bytes -> ${f.absolutePath}", Toast.LENGTH_LONG).show() - val out = Intent() - out.putExtra("bin_path", f.absolutePath) - - setResult(Activity.RESULT_OK, out) - */ - finish() } - //val spinnerMaterialType: Spinner = findViewById(R.id.spinnerMaterialType) - - // 1. Load the abbreviations from material_type_enum.yaml val materialTypes = loadMaterialTypesFromYaml("data/material_type_enum.yaml", "abbreviation").toMutableList() - // 2. Add your prompt to the very first position (index 0) - //materialTypes.add(0, "Select Material Type...") - - // 2. Create the Adapter - // 2. Use the Custom Hint Adapter - /* - val adapter = HintSpinnerAdapter( - this, - android.R.layout.simple_spinner_item, - materialTypes - ) - */ + // Create the Adapter val adapter = ArrayAdapter(this, R.layout.list_item, materialTypes) autoCompleteMaterialType.setAdapter(adapter) autoCompleteMaterialType.setOnClickListener { @@ -763,15 +639,6 @@ class GeneratorActivity : AppCompatActivity() { } } - - /* - // 3. Set the dropdown layout style - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - - // 4. Attach to Spinner - spinnerMaterialType.adapter = adapter - */ - val colorButton = findViewById