Skip to content

Commit 15d28ab

Browse files
committed
Merge feature/generic-data-grouping: UI/UX improvements and tracker restructure
Resolved conflict in release.yml by keeping clean workflow (removed duplicate steps)
2 parents 4eb7a6a + f59c2fb commit 15d28ab

127 files changed

Lines changed: 10039 additions & 1702 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
# Build, sign and upload APK as a GitHub Release asset
21
on:
32
push:
4-
branches:
5-
- '**'
3+
tags:
4+
- "v*"
65
workflow_dispatch:
76

87
permissions:
98
contents: write
109

11-
name: Build, Sign and Release APK
10+
name: Android Release
1211

1312
jobs:
1413
build-and-release:
1514
runs-on: ubuntu-latest
16-
env:
17-
SIGNED_APK_DIR: signed-apks
18-
RELEASE_TAG: apk-${{ github.sha }}
1915
steps:
20-
- name: Checkout code
16+
- name: Checkout
2117
uses: actions/checkout@v4
2218
with:
2319
fetch-depth: 0
@@ -28,61 +24,60 @@ jobs:
2824
distribution: temurin
2925
java-version: '21'
3026

31-
- name: Stop any existing Gradle daemons
32-
run: ./gradlew --stop || true
33-
- name: Cache Gradle
34-
uses: actions/cache@v4
35-
with:
36-
path: |
37-
~/.gradle/caches
38-
~/.gradle/wrapper
39-
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
40-
restore-keys: |
41-
gradle-${{ runner.os }}-
27+
- name: Set up Gradle
28+
uses: gradle/actions/setup-gradle@v4
4229

4330
- name: Decode keystore (if provided)
4431
run: |
4532
if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ]; then
46-
echo "KEYSTORE_BASE64 not set — skipping keystore decode (build will be unsigned)"
33+
echo "KEYSTORE_BASE64 not set — continuing without keystore (build will be unsigned)"
4734
exit 0
4835
fi
49-
# create runner temp directory path and decode keystore there
36+
mkdir -p "$RUNNER_TEMP"
5037
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > "$RUNNER_TEMP/keystore.jks"
38+
chmod 600 "$RUNNER_TEMP/keystore.jks"
5139
echo "Keystore decoded to $RUNNER_TEMP/keystore.jks"
52-
- name: Build release APK and AAB
40+
41+
- name: Debug: verify files before build
42+
run: |
43+
echo "JAVA_HOME=$JAVA_HOME"
44+
java -version
45+
./gradlew -version
46+
47+
- name: Build release APK and AAB (signed if keystore present)
5348
env:
49+
ANDROID_KEYSTORE_PATH: $RUNNER_TEMP/keystore.jks
5450
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
5551
ANDROID_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
5652
ANDROID_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
5753
run: |
58-
# If keystore was decoded, RUNNER_TEMP/keystore.jks exists. Use injected signing props so no keystore info is committed.
59-
STORE_FILE="$RUNNER_TEMP/keystore.jks"
54+
STORE="$ANDROID_KEYSTORE_PATH"
6055
STORE_ARG=""
61-
if [ -f "$STORE_FILE" ]; then
62-
STORE_ARG="-Pandroid.injected.signing.store.file=$STORE_FILE -Pandroid.injected.signing.store.password=$ANDROID_KEYSTORE_PASSWORD -Pandroid.injected.signing.key.alias=$ANDROID_KEY_ALIAS -Pandroid.injected.signing.key.password=$ANDROID_KEY_PASSWORD"
63-
echo "Signing with keystore at $STORE_FILE"
56+
if [ -f "$STORE" ]; then
57+
# Use Android's injected signing properties so you don't have to commit the keystore
58+
STORE_ARG="-Pandroid.injected.signing.store.file=$STORE -Pandroid.injected.signing.store.password=$ANDROID_KEYSTORE_PASSWORD -Pandroid.injected.signing.key.alias=$ANDROID_KEY_ALIAS -Pandroid.injected.signing.key.password=$ANDROID_KEY_PASSWORD"
59+
echo "Will sign using keystore at $STORE"
6460
else
65-
echo "No keystore found at $STORE_FILE; building unsigned artifacts"
61+
echo "No keystore found at $STORE; building unsigned artifacts"
6662
fi
67-
# Force Gradle to use the setup-java JDK on the runner in case project has local overrides
68-
./gradlew assembleRelease bundleRelease -Dorg.gradle.java.home="$JAVA_HOME" $STORE_ARG
69-
- name: Remove keystore file if present
70-
if: always()
63+
64+
# Force Gradle to use the runner JDK and do a clean build
65+
./gradlew --no-daemon clean assembleRelease bundleRelease -Dorg.gradle.java.home="$JAVA_HOME" $STORE_ARG
66+
67+
- name: Show built artifacts (debug)
7168
run: |
72-
if [ -f "$RUNNER_TEMP/keystore.jks" ]; then
73-
shred -u "$RUNNER_TEMP/keystore.jks" || rm -f "$RUNNER_TEMP/keystore.jks"
74-
echo "Keystore removed"
75-
else
76-
echo "No keystore to remove"
77-
fi
69+
echo "APK files:"
70+
ls -al app/build/outputs/apk/release || true
71+
echo "AAB files:"
72+
ls -al app/build/outputs/bundle/release || true
7873
79-
- name: Upload APK artifact
74+
- name: Upload APK artifact (Actions artifact - optional)
8075
uses: actions/upload-artifact@v4
8176
with:
8277
name: simple-data-entry-apk
8378
path: app/build/outputs/apk/release/*.apk
8479

85-
- name: Upload AAB artifact
80+
- name: Upload AAB artifact (Actions artifact - optional)
8681
uses: actions/upload-artifact@v4
8782
with:
8883
name: simple-data-entry-aab
@@ -92,11 +87,23 @@ jobs:
9287
if: ${{ startsWith(github.ref, 'refs/tags/') }}
9388
uses: ncipollo/release-action@v1
9489
with:
95-
9690
tag: ${{ github.ref_name }}
9791
name: "Release ${{ github.ref_name }}"
9892
body: |
9993
Release created by workflow run ${{ github.run_id }} for commit ${{ github.sha }}.
10094
files: |
10195
app/build/outputs/apk/release/*.apk
10296
app/build/outputs/bundle/release/*.aab
97+
98+
env:
99+
GITHUB_TOKEN: ${{ github.token }}
100+
101+
- name: Remove decoded keystore (always)
102+
if: always()
103+
run: |
104+
if [ -f "$RUNNER_TEMP/keystore.jks" ]; then
105+
shred -u "$RUNNER_TEMP/keystore.jks" || rm -f "$RUNNER_TEMP/keystore.jks"
106+
echo "Keystore removed"
107+
else
108+
echo "No keystore to remove"
109+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ build/reports/
130130

131131
# Documentation and analysis files
132132
docs/
133+
design-system/
133134

134135
# Ignore everything in .taskmaster except .gitignore
135136

.idea/deploymentTargetSelector.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CLAUDE.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SimpleDataEntry DHIS2 Android App - Claude Context
22

3-
**Last Updated**: 2025-12-17
4-
**Status**: Production-ready for datasets and tracker enrollments. Login flow resilient. Nested accordion data entry fully functional.
3+
**Last Updated**: 2026-01-20
4+
**Status**: Production-ready for datasets and tracker enrollments. Login flow resilient. Nested accordion data entry fully functional. Implied grouping supports complex patterns.
55

66
## Project Overview
77

@@ -339,6 +339,32 @@ App Launch
339339

340340
---
341341

342+
### Recent Work (2026-01-20)
343+
344+
**Implied Grouping Enhancement** - Hyphen-Suffix Pattern Support:
345+
- **File**: `domain/grouping/ImpliedCategoryInferenceService.kt`
346+
- Added `HYPHEN_SUFFIX_GENDER_REGEX` pattern for complex data element names
347+
- Supports patterns like `WFP - Number of teachers in the school-Qualified Female`
348+
- Creates 3-level hierarchy: Indicator → Qualifier → Gender
349+
- New method: `tryInferWithHyphenSuffixGender()` with mapping support
350+
351+
**Build Configuration** - CI Compatibility:
352+
- **File**: `settings.gradle.kts` - Added foojay-resolver-convention plugin for toolchain auto-download
353+
- **File**: `local.properties` - Contains `org.gradle.java.home` for local development (gitignored)
354+
- System Java 24 not compatible with Gradle 8.11.1; use Android Studio's bundled JDK 21
355+
- CI uses GitHub Actions Java 21 + foojay resolver
356+
357+
### Known Issues (2026-01-20)
358+
359+
**Validation Stack Overflow** (`domain/validation/ValidationService.kt`):
360+
- ⚠️ DHIS2 SDK validation engine hits StackOverflowError on complex rule expressions
361+
- Current behavior: Catches error and returns warning, allows completion
362+
- Root cause: Deep recursion in SDK expression parser (stack size 1035KB insufficient)
363+
- Potential fix: Run validation on thread with larger stack (not yet implemented)
364+
- Log signature: `"DHIS2 SDK validation engine stack overflow: stack size 1035KB"`
365+
366+
---
367+
342368
### Previous Work (2025-12-11)
343369

344370
**Design System Bug Fixes** (2026-01-09):

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ dependencies {
8888
implementation 'androidx.compose.ui:ui-tooling-preview'
8989
implementation 'androidx.compose.material3:material3'
9090
implementation 'androidx.navigation:navigation-compose:2.7.7'
91+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.7.3'
9192
// Unit testing
9293
testImplementation 'junit:junit:4.13.2'
9394
testImplementation 'org.mockito:mockito-core:5.7.0'

app/src/main/java/com/ash/simpledataentry/data/DatabaseManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ class DatabaseManager @Inject constructor(
4646
com.ash.simpledataentry.di.AppModule.MIGRATION_3_4,
4747
com.ash.simpledataentry.di.AppModule.MIGRATION_4_5,
4848
com.ash.simpledataentry.di.AppModule.MIGRATION_6_7,
49-
com.ash.simpledataentry.di.AppModule.MIGRATION_7_8
49+
com.ash.simpledataentry.di.AppModule.MIGRATION_7_8,
50+
com.ash.simpledataentry.di.AppModule.MIGRATION_8_9
5051
)
5152
.build()
5253

app/src/main/java/com/ash/simpledataentry/data/DatabaseProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class DatabaseProvider @Inject constructor(
4444
com.ash.simpledataentry.di.AppModule.MIGRATION_3_4,
4545
com.ash.simpledataentry.di.AppModule.MIGRATION_4_5,
4646
com.ash.simpledataentry.di.AppModule.MIGRATION_6_7,
47-
com.ash.simpledataentry.di.AppModule.MIGRATION_7_8
47+
com.ash.simpledataentry.di.AppModule.MIGRATION_7_8,
48+
com.ash.simpledataentry.di.AppModule.MIGRATION_8_9
4849
)
4950
.build()
5051
}

app/src/main/java/com/ash/simpledataentry/data/SessionManager.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
77
import kotlinx.coroutines.withContext
88
import kotlinx.coroutines.runBlocking
99
import kotlinx.coroutines.async
10+
import kotlinx.coroutines.coroutineScope
1011
import kotlinx.coroutines.flow.MutableStateFlow
1112
import kotlinx.coroutines.flow.StateFlow
1213
import kotlinx.coroutines.flow.asStateFlow
@@ -884,9 +885,22 @@ class SessionManager @Inject constructor(
884885
}
885886

886887
// Check what we have after the download attempt
887-
val hasOrgUnits = d2Instance.organisationUnitModule().organisationUnits().blockingCount() > 0
888-
val hasPrograms = d2Instance.programModule().programs().blockingCount() > 0
889-
val hasDatasets = d2Instance.dataSetModule().dataSets().blockingCount() > 0
888+
val (hasOrgUnits, hasPrograms, hasDatasets) = coroutineScope {
889+
val orgUnitsDeferred = async(Dispatchers.IO) {
890+
d2Instance.organisationUnitModule().organisationUnits().blockingCount() > 0
891+
}
892+
val programsDeferred = async(Dispatchers.IO) {
893+
d2Instance.programModule().programs().blockingCount() > 0
894+
}
895+
val datasetsDeferred = async(Dispatchers.IO) {
896+
d2Instance.dataSetModule().dataSets().blockingCount() > 0
897+
}
898+
Triple(
899+
orgUnitsDeferred.await(),
900+
programsDeferred.await(),
901+
datasetsDeferred.await()
902+
)
903+
}
890904
val hasUser = try {
891905
d2Instance.userModule().user().blockingGet() != null
892906
} catch (e: Exception) {

app/src/main/java/com/ash/simpledataentry/data/cache/MetadataCacheService.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class MetadataCacheService @Inject constructor(
8080
}
8181

8282
// 2. Get data elements from Room (already hydrated during login)
83+
val totalInRoom = dataElementDao.count()
8384
val dataElements = dataElementDao.getByIds(allDataElementUids).associateBy { it.id }
8485

8586
// DEBUG: Log what we found
@@ -91,18 +92,30 @@ class MetadataCacheService @Inject constructor(
9192
}
9293

9394
// 3. Get category combos from Room (already hydrated during login)
94-
val categoryCombos = categoryComboDao.getAll().associateBy { it.id }
95+
val neededComboIds = dataElements.values.mapNotNull { it.categoryComboId }.distinct()
96+
val categoryCombos = if (neededComboIds.isEmpty()) {
97+
emptyMap()
98+
} else {
99+
categoryComboDao.getByIds(neededComboIds).associateBy { it.id }
100+
}
95101

96102
// 4. Get category option combos from Room (already hydrated during login)
97-
val categoryOptionCombos = categoryOptionComboDao.getAll().associateBy { it.id }
103+
val categoryOptionCombos = if (neededComboIds.isEmpty()) {
104+
emptyMap()
105+
} else {
106+
categoryOptionComboDao.getByCategoryComboIds(neededComboIds).associateBy { it.id }
107+
}
98108

99109
// 5. Get org units from Room (already hydrated during login)
100110
val orgUnits = organisationUnitDao.getAll().associateBy { it.id }
101111

102112
// 6. NEW STEP: Pre-fetch and map data values using the parsed UI structure
103113
val sdkDataValues = preFetchAndMapDataValues(datasetId, period, orgUnit, attributeOptionCombo)
104114

105-
Log.d("MetadataCacheService", "Optimized data complete: ${sections.size} sections, ${dataElements.size} data elements, ${sdkDataValues.size} data values")
115+
Log.d(
116+
"MetadataCacheService",
117+
"Optimized data complete: ${sections.size} sections, ${dataElements.size}/${totalInRoom} data elements, ${sdkDataValues.size} data values"
118+
)
106119

107120
OptimizedEntryData(
108121
sections = sections,

app/src/main/java/com/ash/simpledataentry/data/local/AppDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import com.ash.simpledataentry.data.local.DataValueDao
2626
TrackerEnrollmentEntity::class,
2727
EventInstanceEntity::class
2828
],
29-
version = 8, // Incremented for tracker enrollment and event instance tables
29+
version = 9, // Incremented for index additions on metadata tables
3030
exportSchema = false
3131
)
3232
abstract class AppDatabase : RoomDatabase() {

0 commit comments

Comments
 (0)