Skip to content

Commit 3a9b264

Browse files
committed
Add unit test suite, KeyStore security orchestrator, permission managers, observability wrappers, CI/CD build configuration, and operational developer documents
1 parent 803f0b7 commit 3a9b264

23 files changed

Lines changed: 1104 additions & 18 deletions

File tree

.github/workflows/build.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: NeuralPulse CI
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
build:
11+
name: Build & Verify
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout Repository
16+
uses: actions/checkout@v4
17+
18+
- name: Set up JDK 21
19+
uses: actions/setup-java@v4
20+
with:
21+
distribution: 'temurin'
22+
java-version: '21'
23+
cache: 'gradle'
24+
25+
- name: Grant execute permission for gradlew
26+
run: chmod +x gradlew
27+
28+
- name: Run Compile & Unit Tests
29+
run: ./gradlew :app:compileDebugKotlin :wear:compileDebugKotlin testDebugUnitTest --no-daemon

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Changelog
2+
3+
All notable changes to the NeuralPulse project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4+
5+
---
6+
7+
## [1.0.0] - 2026-05-25
8+
9+
### Added
10+
* **Edge Digital Signal Processing**: 4th-order Butterworth bandpass filtering and Kurtosis validation on Wear OS (`:wear`).
11+
* **Vascular Compliance (PTT)**: Continuous Pulse Transit Time calculation using ECG R-Wave and PPG peak delta synchronization.
12+
* **Wear OS 7 Glance Widget**: AMOLED pure black Glance widgets using `RemoteCompose` to stream autonomic recovery indices.
13+
* **Local Machine Learning**: Quantized MobileNetV3 food nutrition classification gate (92% score threshold) and 3-frame temporal consensus engine.
14+
* **Android 16 Clinical Sync**: Clinical FHIR observations pipeline via `ClinicalDataOrchestrator` using the Android 16 Health Connect PHR features.
15+
* **Home IoT Automation**: SmartThings/Matter climate integration (`SmartThingsAutomationBridge`) adjusting thermostat states upon verified sleep transitions.
16+
* **Security Hardening**: AES-GCM local encryption via Android KeyStore, secure preferences wrapper, and `BiometricPrompt` authorization before clinical data exports.
17+
* **Network Security Configuration**: SSL certificate pinning configurations for clinical API endpoints.
18+
* **CI/CD Pipeline**: GitHub Actions workflow (`build.yml`) checking compiling and executing local unit tests on every PR.
19+
* **Core Unit Test Suite**: JUnit 4 testing classes for filters, PTT calculations, consensus arrays, and dual-wearable priority rules.
20+
* **Stub Shading Libraries**: Shaded local classes for Samsung Health, Health Connect, and MediaPipe to enable independent compiler bootstrapping.

CONTRIBUTING.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Contributing Guidelines
2+
3+
Thank you for contributing to the NeuralPulse project. To maintain clinical-grade code quality and ensure build safety, please adhere to the following guidelines.
4+
5+
---
6+
7+
## 1. Code Style and Constraints
8+
9+
* **Language Standard**: Kotlin 1.9+ and JDK 17 / 21 compatibility.
10+
* **No Emojis**: Emojis are strictly banned from all source code files, comments, commit messages, and documentation files to ensure standard clinical logs and readability.
11+
* **Annotation Placement**: File-level annotations (such as `@file:Suppress`) must be placed at the absolute top of the source file, preceding the `package` declaration block.
12+
* **Package Shading**: All compile-time stubs must mirror the exact package names of their target library components (e.g. `androidx.health.connect.client` or `com.samsung.android.service.health.tracking`).
13+
14+
---
15+
16+
## 2. Git Workflow & Branching
17+
18+
* **Branches**:
19+
* `master`: Production branch. Keep compiles passing at all times.
20+
* `feature/*`: New features or implementation modules.
21+
* `bugfix/*`: Bug fixes and regression repairs.
22+
* **Pull Request Requirements**:
23+
* All PRs must compile cleanly without errors using:
24+
```bash
25+
./gradlew assembleDebug
26+
```
27+
* All unit tests must pass locally:
28+
```bash
29+
./gradlew testDebugUnitTest
30+
```
31+
* Every pull request automatically triggers the GitHub Actions CI build to verify these checks.
32+
33+
---
34+
35+
## 3. Testing Requirements
36+
37+
* Any modification or addition to signal processing modules, digital filtering, wearable data sync protocols, or security managers **must** be accompanied by unit tests.
38+
* Ensure tests run successfully on local JVMs without requiring external hardware connections or full Android device setups.

app/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ dependencies {
8888
androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
8989
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
9090

91+
// Security & Cryptography
92+
implementation("androidx.security:security-crypto:1.1.0-alpha06")
93+
implementation("androidx.biometric:biometric:1.2.0-alpha05")
94+
9195
debugImplementation("androidx.compose.ui:ui-tooling")
9296
debugImplementation("androidx.compose.ui:ui-test-manifest")
9397
}

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
<uses-permission android:name="android.permission.HEALTH_CONNECT_READ" />
77
<uses-permission android:name="android.permission.HEALTH_CONNECT_WRITE" />
88
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
9+
<uses-permission android:name="android.permission.BODY_SENSORS" />
10+
<uses-permission android:name="android.permission.CAMERA" />
911

1012
<!-- Internet permission for synchronization if required -->
1113
<uses-permission android:name="android.permission.INTERNET" />
@@ -16,7 +18,8 @@
1618
android:label="NeuralPulse"
1719
android:roundIcon="@android:mipmap/sym_def_app_icon"
1820
android:supportsRtl="true"
19-
android:theme="@style/Theme.Material3.Dark">
21+
android:theme="@style/Theme.Material3.Dark"
22+
android:networkSecurityConfig="@xml/network_security_config">
2023

2124
<!-- Samsung Health SDK client identification profile -->
2225
<!-- SHA256 fingerprints are generated during developer registration in the Samsung Partner Portal -->
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.alphahealth.monitor.permissions
2+
3+
import android.Manifest
4+
import android.content.Context
5+
import android.content.pm.PackageManager
6+
import android.os.Build
7+
import androidx.core.content.ContextCompat
8+
9+
class PermissionManager(private val context: Context) {
10+
11+
private val requiredPermissions = mutableListOf(
12+
Manifest.permission.CAMERA,
13+
Manifest.permission.ACTIVITY_RECOGNITION
14+
).apply {
15+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
16+
add(Manifest.permission.BODY_SENSORS)
17+
}
18+
}
19+
20+
/**
21+
* Checks if all standard runtime permissions are granted.
22+
*/
23+
fun hasRequiredPermissions(): Boolean {
24+
return requiredPermissions.all {
25+
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
26+
}
27+
}
28+
29+
/**
30+
* Retrieves the lists of ungranted required permissions.
31+
*/
32+
fun getUngrantedPermissions(): List<String> {
33+
return requiredPermissions.filter {
34+
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
35+
}
36+
}
37+
38+
/**
39+
* Provides a clear clinical rationale for each requested permission to aid user UX.
40+
*/
41+
fun getPermissionRationale(permission: String): String {
42+
return when (permission) {
43+
Manifest.permission.CAMERA -> {
44+
"The camera is required for the local on-device computer vision engine to scan foods and extract dietary profiles."
45+
}
46+
Manifest.permission.BODY_SENSORS -> {
47+
"Body sensors access is required to read raw high-frequency biometric streams (such as EDA, PPG) directly from your wearable devices."
48+
}
49+
Manifest.permission.ACTIVITY_RECOGNITION -> {
50+
"Activity recognition is used to track motion contexts and gait kinematics, ensuring accurate calorie calculations."
51+
}
52+
else -> "Required for core homeostatic recovery and health metrics tracking."
53+
}
54+
}
55+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.alphahealth.monitor.security
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import android.security.keystore.KeyGenParameterSpec
6+
import android.security.keystore.KeyProperties
7+
import android.util.Base64
8+
import androidx.biometric.BiometricPrompt
9+
import androidx.core.content.ContextCompat
10+
import androidx.security.crypto.EncryptedSharedPreferences
11+
import androidx.security.crypto.MasterKey
12+
import java.security.KeyStore
13+
import javax.crypto.Cipher
14+
import javax.crypto.KeyGenerator
15+
import javax.crypto.SecretKey
16+
import javax.crypto.spec.GCMParameterSpec
17+
18+
class SecurityOrchestrator(private val context: Context) {
19+
20+
private val KEY_ALIAS = "NeuralPulseClinicalKey"
21+
private val ANDROID_KEYSTORE = "AndroidKeyStore"
22+
private val TRANSFORMATION = "AES/GCM/NoPadding"
23+
24+
init {
25+
generateSecretKeyIfNeeded()
26+
}
27+
28+
private fun generateSecretKeyIfNeeded() {
29+
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
30+
if (!keyStore.containsAlias(KEY_ALIAS)) {
31+
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
32+
val parameterSpec = KeyGenParameterSpec.Builder(
33+
KEY_ALIAS,
34+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
35+
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
36+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
37+
.setKeySize(256)
38+
.build()
39+
keyGenerator.init(parameterSpec)
40+
keyGenerator.generateKey()
41+
}
42+
}
43+
44+
private fun getSecretKey(): SecretKey {
45+
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
46+
return keyStore.getKey(KEY_ALIAS, null) as SecretKey
47+
}
48+
49+
/**
50+
* Encrypts clinical data string using AES-GCM encryption.
51+
*/
52+
fun encryptData(plainText: String): String {
53+
val cipher = Cipher.getInstance(TRANSFORMATION)
54+
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
55+
val encryptionIv = cipher.iv
56+
val cipherText = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
57+
58+
// Combine IV and cipherText to store/transmit together
59+
val combined = ByteArray(encryptionIv.size + cipherText.size)
60+
System.arraycopy(encryptionIv, 0, combined, 0, encryptionIv.size)
61+
System.arraycopy(cipherText, 0, combined, encryptionIv.size, cipherText.size)
62+
63+
return Base64.encodeToString(combined, Base64.DEFAULT)
64+
}
65+
66+
/**
67+
* Decrypts GCM-encrypted clinical data using Android KeyStore secret keys.
68+
*/
69+
fun decryptData(encryptedBase64: String): String {
70+
val combined = Base64.decode(encryptedBase64, Base64.DEFAULT)
71+
val ivSize = 12 // Standard AES-GCM IV length is 12 bytes
72+
val cipherTextSize = combined.size - ivSize
73+
74+
val iv = ByteArray(ivSize)
75+
val cipherText = ByteArray(cipherTextSize)
76+
System.arraycopy(combined, 0, iv, 0, ivSize)
77+
System.arraycopy(combined, ivSize, cipherText, 0, cipherTextSize)
78+
79+
val cipher = Cipher.getInstance(TRANSFORMATION)
80+
val spec = GCMParameterSpec(128, iv)
81+
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
82+
83+
val decrypted = cipher.doFinal(cipherText)
84+
return String(decrypted, Charsets.UTF_8)
85+
}
86+
87+
/**
88+
* Obtains secure EncryptedSharedPreferences to store configuration properties like API tokens.
89+
*/
90+
fun getSecurePreferences(): SharedPreferences {
91+
val masterKey = MasterKey.Builder(context)
92+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
93+
.build()
94+
95+
return EncryptedSharedPreferences.create(
96+
context,
97+
"neuralpulse_secure_prefs",
98+
masterKey,
99+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
100+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
101+
)
102+
}
103+
104+
/**
105+
* Launches the biometric prompt to authenticate the user before sensitive actions (e.g. data export).
106+
*/
107+
fun showBiometricAuthentication(
108+
activity: androidx.fragment.app.FragmentActivity,
109+
title: String,
110+
subtitle: String,
111+
onSuccess: () -> Unit,
112+
onFailure: (String) -> Unit
113+
) {
114+
val executor = ContextCompat.getMainExecutor(context)
115+
val biometricPrompt = BiometricPrompt(activity, executor,
116+
object : BiometricPrompt.AuthenticationCallback() {
117+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
118+
super.onAuthenticationSucceeded(result)
119+
onSuccess()
120+
}
121+
122+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
123+
super.onAuthenticationError(errorCode, errString)
124+
onFailure(errString.toString())
125+
}
126+
127+
override fun onAuthenticationFailed() {
128+
super.onAuthenticationFailed()
129+
onFailure("Biometric authentication failed.")
130+
}
131+
})
132+
133+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
134+
.setTitle(title)
135+
.setSubtitle(subtitle)
136+
.setNegativeButtonText("Cancel")
137+
.build()
138+
139+
biometricPrompt.authenticate(promptInfo)
140+
}
141+
}

app/src/main/java/com/alphahealth/monitor/vision/HighPrecisionClassifier.kt

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ class HighPrecisionClassifier(private val context: Context) {
1313

1414
private val TAG = "HighPrecisionClassifier"
1515
private var classifier: ImageClassifier? = null
16-
// Temporal frame buffer tracking predictions over time
17-
private val predictionHistory = mutableListOf<Map<String, Float>>()
16+
private val consensusEngine = TemporalConsensusEngine(windowSize = 3, requiredAgreement = 2)
1817

1918
init {
2019
setupClassifier()
@@ -47,7 +46,7 @@ class HighPrecisionClassifier(private val context: Context) {
4746

4847
val topCategory = results?.classificationResult()?.classifications()?.firstOrNull()?.categories()?.firstOrNull()
4948
if (topCategory != null && topCategory.score() >= 0.92f) {
50-
applyTemporalConsensus(topCategory.categoryName(), topCategory.score())
49+
consensusEngine.addPredictionAndCheckConsensus(topCategory.categoryName())
5150
} else {
5251
null
5352
}
@@ -56,18 +55,4 @@ class HighPrecisionClassifier(private val context: Context) {
5655
null
5756
}
5857
}
59-
60-
private fun applyTemporalConsensus(name: String, score: Float): String? {
61-
// Simple 3-frame moving consensus checking window to eliminate fleeting false-positives
62-
if (predictionHistory.size >= 3) {
63-
predictionHistory.removeAt(0)
64-
}
65-
predictionHistory.add(mapOf(name to score))
66-
67-
val frequencyMap = predictionHistory.flatMap { it.keys }.groupingBy { it }.eachCount()
68-
val mostFrequent = frequencyMap.maxByOrNull { it.value }
69-
70-
// Only commit the change to the user's database if the food item is stable across sequential frames
71-
return if (mostFrequent != null && mostFrequent.value >= 2) mostFrequent.key else null
72-
}
7358
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.alphahealth.monitor.vision
2+
3+
class TemporalConsensusEngine(
4+
private val windowSize: Int = 3,
5+
private val requiredAgreement: Int = 2
6+
) {
7+
private val predictionHistory = mutableListOf<String>()
8+
9+
/**
10+
* Integrates a new prediction and checks if a consensus has been reached.
11+
* @param name The prediction item name.
12+
* @return The item name if a consensus is reached, null otherwise.
13+
*/
14+
fun addPredictionAndCheckConsensus(name: String): String? {
15+
if (predictionHistory.size >= windowSize) {
16+
predictionHistory.removeAt(0)
17+
}
18+
predictionHistory.add(name)
19+
20+
val frequencyMap = predictionHistory.groupingBy { it }.eachCount()
21+
val mostFrequent = frequencyMap.maxByOrNull { it.value }
22+
23+
return if (mostFrequent != null && mostFrequent.value >= requiredAgreement) {
24+
mostFrequent.key
25+
} else {
26+
null
27+
}
28+
}
29+
30+
/**
31+
* Resets the prediction history.
32+
*/
33+
fun clearHistory() {
34+
predictionHistory.clear()
35+
}
36+
}

0 commit comments

Comments
 (0)