HydroFlux 0.0.1

This commit is contained in:
2026-02-04 20:56:57 +11:00
commit 5a8c661ce8
88 changed files with 7464 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
package com.david.hydroflux
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebChromeClient
import android.widget.Toast // Added for Debugging
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.time.TimeRangeFilter
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
class MainActivity : FragmentActivity() {
private lateinit var myWebView: WebView
private lateinit var healthConnectClient: HealthConnectClient
// Define Permissions we need
private val PERMISSIONS = setOf(
HealthPermission.getReadPermission(StepsRecord::class),
HealthPermission.getReadPermission(SleepSessionRecord::class)
)
private val requestPermissions = registerForActivityResult(
androidx.health.connect.client.PermissionController.createRequestPermissionResultContract()
) { granted ->
if (granted.containsAll(PERMISSIONS)) {
// Permissions Granted - Silent Sync
if (::healthConnectClient.isInitialized) {
// Use a slight delay to allow permission propogation
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
WebAppInterface(this@MainActivity).triggerSync()
}, 500)
}
} else {
val missing = PERMISSIONS.minus(granted)
Toast.makeText(this, "Denied. Opening Settings...", Toast.LENGTH_LONG).show()
// Fallback: Open Health Connect Settings so user can enable manually
try {
val intent = android.content.Intent("androidx.health.ACTION_HEALTH_CONNECT_SETTINGS")
startActivity(intent)
} catch (e: Exception) {
// Try generic app details
Toast.makeText(this, "Could not open Health Settings", Toast.LENGTH_SHORT).show()
val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = android.net.Uri.parse("package:$packageName")
startActivity(intent)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Setup WebView FIRST to ensure we have it
myWebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true
myWebView.settings.domStorageEnabled = true
myWebView.settings.allowFileAccess = true
myWebView.settings.allowFileAccessFromFileURLs = true
myWebView.settings.allowUniversalAccessFromFileURLs = true
myWebView.webViewClient = WebViewClient()
myWebView.webChromeClient = WebChromeClient()
myWebView.addJavascriptInterface(WebAppInterface(this), "HydroFluxNative")
myWebView.loadUrl("file:///android_asset/index.html")
// Setup Health Connect
val availabilityStatus = HealthConnectClient.getSdkStatus(this)
if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
Toast.makeText(this, "Health Connect Unavailable on this device", Toast.LENGTH_LONG).show()
return
}
if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
Toast.makeText(this, "Health Connect Update Required", Toast.LENGTH_LONG).show()
return
}
// Grab client
try {
healthConnectClient = HealthConnectClient.getOrCreate(this)
} catch (e: Exception) {
e.printStackTrace()
}
// Start Periodic Sleep Check (6am - 10am)
startPeriodicCheck()
}
private fun startPeriodicCheck() {
val handler = android.os.Handler(android.os.Looper.getMainLooper())
val runnable = object : Runnable {
override fun run() {
val cal = java.util.Calendar.getInstance()
val hour = cal.get(java.util.Calendar.HOUR_OF_DAY)
// Check between 6:00 and 10:00
if (hour in 6..10) {
WebAppInterface(this@MainActivity).triggerSync()
}
// Repeat every 30 minutes (30 * 60 * 1000)
handler.postDelayed(this, 1800000)
}
}
handler.post(runnable) // Start immediately
}
inner class WebAppInterface(private val activity: MainActivity) {
@JavascriptInterface
fun requestHealthPermissions() {
runOnUiThread {
if (!::healthConnectClient.isInitialized) {
Toast.makeText(activity, "Health Client Not Init", Toast.LENGTH_SHORT).show()
return@runOnUiThread
}
activity.lifecycleScope.launch {
try {
val granted = healthConnectClient.permissionController.getGrantedPermissions()
if (granted.containsAll(PERMISSIONS)) {
triggerSync()
} else {
try {
requestPermissions.launch(PERMISSIONS)
} catch (e: Exception) {
val err = e.message ?: "Unknown"
Toast.makeText(activity, "Launch Error: $err", Toast.LENGTH_LONG).show()
e.printStackTrace()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
@JavascriptInterface
fun triggerSync() {
if (!::healthConnectClient.isInitialized) return
activity.lifecycleScope.launch {
// MOVE TO IO THREAD for stability (Prevents IllegalStateException)
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
// Use Application Context for Client
try {
if (!::healthConnectClient.isInitialized) {
healthConnectClient = HealthConnectClient.getOrCreate(activity.applicationContext)
}
} catch (e: Exception) {
e.printStackTrace()
}
val startOfDay = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toInstant()
val now = Instant.now()
var totalSteps = 0L
var sleepHours = 0.0
// 1. Try Read Steps (AGGREGATE)
try {
val response = healthConnectClient.aggregate(
AggregateRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(startOfDay, now)
)
)
// The result may be null if no data
totalSteps = response[StepsRecord.COUNT_TOTAL] ?: 0
} catch (e: Exception) {
e.printStackTrace()
val msg = e.message ?: "Unknown Error"
runOnUiThread { Toast.makeText(activity, "Steps Error: $msg", Toast.LENGTH_LONG).show() }
totalSteps = 0
}
// 2. Try Read Sleep (AGGREGATE)
try {
val sleepResponse = healthConnectClient.aggregate(
AggregateRequest(
metrics = setOf(SleepSessionRecord.SLEEP_DURATION_TOTAL),
timeRangeFilter = TimeRangeFilter.between(now.minus(24, ChronoUnit.HOURS), now)
)
)
// Get total duration in Seconds (Duration -> Seconds)
val duration = sleepResponse[SleepSessionRecord.SLEEP_DURATION_TOTAL]
if (duration != null) {
sleepHours = duration.seconds / 3600.0
}
} catch (e: Exception) {
e.printStackTrace()
sleepHours = 0.0
}
runOnUiThread {
if (!activity.isFinishing && !activity.isDestroyed) {
try {
myWebView.evaluateJavascript("window.updateHealthData($totalSteps, $sleepHours)", null)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
}
}
@Suppress("DEPRECATION")
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (myWebView.canGoBack()) {
myWebView.goBack()
} else {
super.onBackPressed()
}
}
}

View File

@@ -0,0 +1,54 @@
package com.david.hydroflux
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Button
import android.view.Gravity
import android.graphics.Color
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class PermissionsRationaleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val title = TextView(this).apply {
text = "Health Permission Needed"
textSize = 24f
setTextColor(Color.CYAN)
gravity = Gravity.CENTER
setPadding(0, 0, 0, 30)
}
val message = TextView(this).apply {
text = "HydroFit needs access to your Steps and Sleep data to populate the activity rings and history charts.\n\nWe do not share this data."
textSize = 16f
setTextColor(Color.WHITE)
gravity = Gravity.CENTER
setPadding(0, 0, 0, 50)
}
val btn = Button(this).apply {
text = "UNDERSTOOD"
setBackgroundColor(Color.DKGRAY)
setTextColor(Color.WHITE)
setOnClickListener {
finish() // Close rationale and return to system dialog logic
}
}
val layout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setPadding(50, 50, 50, 50)
setBackgroundColor(Color.BLACK)
addView(title)
addView(message)
addView(btn)
}
setContentView(layout)
}
}

View File

@@ -0,0 +1,11 @@
package com.david.hydroflux.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,58 @@
package com.david.hydroflux.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun HydrofluxTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,34 @@
package com.david.hydroflux.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)