Fixed show_content bug. Buttons cancel and continue doesn't work.

This commit is contained in:
2026-06-05 23:30:34 +02:00
parent 19ba25715a
commit 9133103e82
11 changed files with 489 additions and 134 deletions
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
-1
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+21
View File
@@ -2,8 +2,17 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
} }
val localPropertiesFile = rootProject.file("local.properties")
val nocobaseToken = localPropertiesFile.readLines()
.firstOrNull { it.startsWith("NOCOBASE_TOKEN=") }
?.removePrefix("NOCOBASE_TOKEN=") ?: ""
val nocobaseUrl = localPropertiesFile.readLines()
.firstOrNull { it.startsWith("NOCOBASE_URL=") }
?.removePrefix("NOCOBASE_URL=") ?: ""
android { android {
namespace = "com.example.ichwillheim" namespace = "com.example.ichwillheim"
compileSdk { compileSdk {
@@ -18,6 +27,9 @@ android {
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "NOCOBASE_TOKEN", "\"$nocobaseToken\"")
buildConfigField("String", "NOCOBASE_URL", "\"$nocobaseUrl\"")
} }
buildTypes { buildTypes {
@@ -38,18 +50,27 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation("androidx.compose.material:material-icons-extended")
implementation(libs.kotlinx.serialization.json)
implementation("androidx.savedstate:savedstate:1.4.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("io.ktor:ktor-client-android:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
+20
View File
@@ -2,6 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -20,8 +33,15 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".CallOverlayService"
android:foregroundServiceType="specialUse"
android:exported="false" />
</application> </application>
</manifest> </manifest>
@@ -1,36 +1,51 @@
package com.example.ichwillheim package com.example.ichwillheim
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore
import android.provider.Settings
import android.telephony.TelephonyManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.core.content.ContextCompat
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import kotlinx.coroutines.launch
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.ichwillheim.ui.theme.IchWillHeimTheme
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.example.ichwillheim.ui.theme.IchWillHeimTheme
class MainActivity : ComponentActivity() { class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
@@ -47,59 +62,217 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun LauncherScreen(modifier: Modifier = Modifier) { fun LauncherScreen(modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
val favorites = listOf( val scope = rememberCoroutineScope()
FavoriteContact(
name = "Sandro",
phone = "+1-555-0100"
),
FavoriteContact(
name = "Paulo",
phone = "+1-555-0111"
)
)
val apps = listOf( var settings by remember { mutableStateOf(SettingsRepository.load(context)) }
AppEntry(
label = "Messages",
action = { openMessages(context) }
)
)
Column( // Request CALL_PHONE permission at startup if not already granted.
// CALL_PHONE is a "dangerous" permission — having it in the manifest is not enough on
// Android 6+; the user must explicitly grant it the first time.
val callPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { /* result is ignored — if denied, performCall will show an error at call time */ }
LaunchedEffect(Unit) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED
) {
callPermissionLauncher.launch(Manifest.permission.CALL_PHONE)
}
}
// Navigation flags — only one is true at a time.
var showPin by remember { mutableStateOf(false) }
var showCaregiverMenu by remember { mutableStateOf(false) }
var showLauncherSettings by remember { mutableStateOf(false) }
var activeRun by remember { mutableStateOf<WorkflowRunState?>(null) }
var stepStartTime by remember { mutableStateOf(System.currentTimeMillis()) }
LaunchedEffect(activeRun?.currentStep?.id) { stepStartTime = System.currentTimeMillis() }
// Fetch remote config on every resume so the home screen stays up-to-date when
// the caregiver adds or changes workflows without restarting the app.
// Also resets sub-screen navigation so the patient always returns to the home screen.
val settingsRef = rememberUpdatedState(settings)
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
showPin = false
showCaregiverMenu = false
showLauncherSettings = false
activeRun = null
if (settingsRef.value.deviceId.isNotBlank()) {
scope.launch {
val remote = RemoteConfigRepository.fetchWorkflows(settingsRef.value.deviceId)
if (remote != null) {
val updated = settingsRef.value.copy(workflows = remote)
SettingsRepository.save(context, updated)
settings = updated
}
}
}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
val anySubScreenOpen = showPin || showCaregiverMenu || showLauncherSettings || activeRun != null
BackHandler(enabled = !anySubScreenOpen) { /* swallow back on main screen */ }
if (activeRun != null) {
val run = activeRun!!
android.util.Log.d("WorkflowEngine", "Step: ${run.currentStep?.functionKey}, finished: ${run.isFinished}")
when (run.currentStep?.functionKey) {
"show_call_confirm" -> WorkflowConfirmScreen(
title = run.workflow.name,
onConfirm = {
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
val next = run.advance("confirmed")
activeRun = next.takeIf { !next.isFinished }
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, "confirmed", duration)
}
}
},
onCancel = {
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
activeRun = null
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, "cancelled", duration)
}
}
}
)
"show_speaker_choice" -> WorkflowSpeakerChoiceScreen(
onChoice = { choice ->
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
val next = run.advance(choice)
activeRun = next.takeIf { !next.isFinished }
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, choice, duration)
}
}
},
onCancel = {
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
activeRun = null
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, "cancelled", duration)
}
}
}
)
"call_contact" -> LaunchedEffect(run.currentStep!!.id) {
val completedStep = run.currentStep!!
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
val phone = run.param("phone") ?: ""
val speakerphone = run.param("speakerphone")?.toBooleanStrictOrNull()
?: (run.lastResultCode == "speakerphone")
android.util.Log.d("WorkflowEngine", "call_contact: phone='$phone' speakerphone=$speakerphone")
if (phone.isBlank()) {
Toast.makeText(context, "No phone number set for this step. Add a contact in the web app.", Toast.LENGTH_LONG).show()
} else {
performCall(context, phone, speakerphone, showOverlay = false)
}
val next = run.advance(null)
activeRun = next.takeIf { !next.isFinished }
if (settings.deviceId.isNotBlank()) {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, null, duration)
}
}
"show_content" -> ContentScreen(
blocks = run.currentStep?.contentBlocks ?: emptyList(),
onAdvance = { resultCode ->
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
val next = run.advance(resultCode)
activeRun = next.takeIf { !next.isFinished }
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(
settings.deviceId, run.workflow.name,
completedStep.functionKey, resultCode, duration
)
}
}
},
onCancel = {
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
activeRun = null
if (completedStep != null && settings.deviceId.isNotBlank()) {
scope.launch {
RemoteConfigRepository.logEvent(
settings.deviceId, run.workflow.name,
completedStep.functionKey, "cancelled", duration
)
}
}
}
)
else -> LaunchedEffect(run.currentStep?.id) {
val completedStep = run.currentStep
val duration = (System.currentTimeMillis() - stepStartTime) / 1000.0
val next = run.advance(null)
activeRun = next.takeIf { !next.isFinished }
if (completedStep != null && settings.deviceId.isNotBlank()) {
RemoteConfigRepository.logEvent(settings.deviceId, run.workflow.name, completedStep.functionKey, null, duration)
}
}
}
return
}
if (showLauncherSettings) {
LauncherSettingsScreen(
settings = settings,
onSettingsChanged = { updated ->
settings = updated
SettingsRepository.save(context, updated)
},
onClose = { showLauncherSettings = false }
)
return
}
if (showCaregiverMenu) {
SettingsMenuScreen(
onLauncherSettings = { showLauncherSettings = true },
onPhoneSettings = {
showCaregiverMenu = false
context.startActivity(Intent(Settings.ACTION_SETTINGS))
},
onClose = { showCaregiverMenu = false }
)
return
}
if (showPin) {
PinScreen(
onCorrectPin = {
showPin = false
showCaregiverMenu = true
},
onCancel = { showPin = false }
)
return
}
HomeScreen(
onOpenSettings = { showPin = true },
workflows = settings.workflows,
gridSize = settings.gridSize,
onStartWorkflow = { workflow -> activeRun = WorkflowRunState.start(workflow).takeIf { !it.isFinished } },
modifier = modifier modifier = modifier
.fillMaxSize() )
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Favorites", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
favorites.forEach { favorite ->
Button(
onClick = { openDialerNumber(context, favorite.phone) },
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
) {
Text(text = favorite.name, fontSize = 24.sp)
}
Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Apps", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
apps.forEach { app ->
Button(
onClick = { app.action() },
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
) {
Text(text = app.label, fontSize = 24.sp)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@@ -110,30 +283,23 @@ fun LauncherPreview() {
} }
} }
@Immutable fun openDialerNumber(context: Context, phone: String) {
data class AppEntry(
val label: String,
val action: () -> Unit
)
@Immutable
data class FavoriteContact(
val name: String,
val phone: String
)
private fun openDialerNumber(context: Context, phone: String) {
val intent = Intent(Intent.ACTION_DIAL).apply { val intent = Intent(Intent.ACTION_DIAL).apply {
data = "tel:${Uri.encode(phone)}".toUri() data = "tel:${Uri.encode(phone)}".toUri()
} }
startIntentOrToast(context, intent, "Phone app not found") startIntentOrToast(context, intent, context.getString(R.string.error_phone_app_not_found))
} }
private fun openMessages(context: Context) { private fun openMessages(context: Context) {
val intent = Intent(Intent.ACTION_SENDTO).apply { val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "smsto:".toUri() data = "smsto:".toUri()
} }
startIntentOrToast(context, intent, "Messages app not found") startIntentOrToast(context, intent, context.getString(R.string.error_messages_app_not_found))
}
private fun openCamera(context: Context) {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startIntentOrToast(context, intent, context.getString(R.string.error_camera_app_not_found))
} }
private fun startIntentOrToast(context: Context, intent: Intent, error: String) { private fun startIntentOrToast(context: Context, intent: Intent, error: String) {
@@ -143,3 +309,58 @@ private fun startIntentOrToast(context: Context, intent: Intent, error: String)
Toast.makeText(context, error, Toast.LENGTH_SHORT).show() Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
} }
} }
fun performCall(context: Context, phone: String, speakerphone: Boolean, showOverlay: Boolean) {
val willShowOverlay = showOverlay &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
Settings.canDrawOverlays(context)
// Only use the standalone BroadcastReceiver when the overlay isn't handling speakerphone
if (speakerphone && !willShowOverlay) enableSpeakerphoneOnAnswer(context)
if (willShowOverlay) {
val serviceIntent = Intent(context, CallOverlayService::class.java).apply {
putExtra(EXTRA_SPEAKERPHONE, speakerphone)
}
context.startForegroundService(serviceIntent)
}
val callIntent = Intent(Intent.ACTION_CALL).apply {
data = Uri.parse("tel:$phone")
}
try {
context.startActivity(callIntent)
} catch (_: Exception) {
Toast.makeText(context, context.getString(R.string.error_phone_app_not_found), Toast.LENGTH_SHORT).show()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun enableSpeakerphoneOnAnswer(context: Context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.getStringExtra(TelephonyManager.EXTRA_STATE)
== TelephonyManager.EXTRA_STATE_OFFHOOK) {
setSpeakerphoneOn(ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
ctx.unregisterReceiver(this)
}
}
}
val filter = IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(receiver, filter)
}
}
private fun setSpeakerphoneOn(audioManager: AudioManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val speaker = audioManager.availableCommunicationDevices
.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
if (speaker != null) audioManager.setCommunicationDevice(speaker)
} else {
@Suppress("DEPRECATION")
audioManager.isSpeakerphoneOn = true
}
}
@@ -2,10 +2,9 @@ package com.example.ichwillheim.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) val PeachLight = Color(0xFFFFF6E5)
val PurpleGrey80 = Color(0xFFCCC2DC) val SageGreen = Color(0xFFA6CDB2)
val Pink80 = Color(0xFFEFB8C8) val WarmOrange = Color(0xFFFCAB7E)
val DarkWarm = Color(0xFF2D2D2D)
val Purple40 = Color(0xFF6650a4) val PeachMedium = Color(0xFFE8D4BE)
val PurpleGrey40 = Color(0xFF625b71) val PeachSurface = Color(0xFFFEF3E8)
val Pink40 = Color(0xFF7D5260)
@@ -1,57 +1,33 @@
package com.example.ichwillheim.ui.theme package com.example.ichwillheim.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme 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.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val ColorScheme = lightColorScheme(
primary = Purple80, primary = SageGreen,
secondary = PurpleGrey80, onPrimary = DarkWarm,
tertiary = Pink80 primaryContainer = PeachLight,
) onPrimaryContainer = DarkWarm,
secondary = WarmOrange,
private val LightColorScheme = lightColorScheme( onSecondary = DarkWarm,
primary = Purple40, secondaryContainer = PeachLight,
secondary = PurpleGrey40, onSecondaryContainer = DarkWarm,
tertiary = Pink40 tertiary = PeachLight,
onTertiary = DarkWarm,
/* Other default colors to override background = PeachLight,
background = Color(0xFFFFFBFE), onBackground = DarkWarm,
surface = Color(0xFFFFFBFE), surface = PeachLight,
onPrimary = Color.White, onSurface = DarkWarm,
onSecondary = Color.White, surfaceVariant = PeachSurface,
onTertiary = Color.White, onSurfaceVariant = DarkWarm,
onBackground = Color(0xFF1C1B1F), outline = PeachMedium,
onSurface = Color(0xFF1C1B1F),
*/
) )
@Composable @Composable
fun IchWillHeimTheme( fun IchWillHeimTheme(content: @Composable () -> Unit) {
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( MaterialTheme(
colorScheme = colorScheme, colorScheme = ColorScheme,
typography = Typography, typography = Typography,
content = content content = content
) )
+113
View File
@@ -1,3 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">IchWillHeim</string> <string name="app_name">IchWillHeim</string>
<!-- Common actions -->
<string name="action_cancel">Cancel</string>
<string name="action_done">Done</string>
<string name="action_save">Save</string>
<string name="action_confirm">Confirm</string>
<string name="action_next">Next</string>
<string name="action_close">Close</string>
<string name="action_call">Call</string>
<string name="action_open_settings">Open settings</string>
<string name="action_add_contact">Add Contact</string>
<string name="action_add_guide">Add Guide</string>
<string name="action_add_step">Add Step</string>
<string name="action_edit_help_screen">Edit Help Screen</string>
<!-- Content descriptions -->
<string name="cd_previous">Previous</string>
<string name="cd_next">Next</string>
<string name="cd_back">Back</string>
<string name="cd_caregiver_settings">Settings access</string>
<string name="cd_edit_contact">Edit %1$s</string>
<string name="cd_delete_contact">Delete %1$s</string>
<string name="cd_edit_steps">Edit steps</string>
<string name="cd_delete_guide">Delete guide</string>
<string name="cd_edit_step">Edit step</string>
<string name="cd_delete_step">Delete step</string>
<!-- Home screen -->
<string name="home_new_messages">You have new messages</string>
<string name="btn_contacts">Contacts</string>
<string name="btn_messages">Messages</string>
<string name="btn_camera">Camera</string>
<string name="photo_placeholder">Photo %1$d</string>
<!-- Favorites screen -->
<string name="favorites_who_to_call">Who would you like to call?</string>
<!-- Caregiver menu -->
<string name="caregiver_menu_title">Settings Menu</string>
<string name="launcher_settings_title">Settings</string>
<string name="caregiver_menu_phone_settings">Phone Settings</string>
<!-- PIN screen -->
<string name="pin_screen_title">Settings Access</string>
<string name="field_pin">PIN</string>
<string name="pin_error_incorrect">Incorrect PIN</string>
<!-- Launcher settings -->
<string name="edit_contacts_title">Edit Contacts</string>
<string name="settings_language_section">Language</string>
<string name="settings_language_system">System default</string>
<string name="settings_device_section">Device</string>
<string name="settings_device_id">Device ID</string>
<string name="settings_call_flow_section">Call flow</string>
<string name="settings_call_flow_description">Choose which steps are shown before each call.</string>
<string name="settings_call_overlay_section">Call overlay</string>
<string name="settings_call_overlay_label">Overlay during call</string>
<string name="step_reminders_label">Reminders</string>
<string name="step_reminders_desc">Important information before the call</string>
<string name="step_help_offer_label">Help offer</string>
<string name="step_help_offer_desc">Ask if the patient needs help</string>
<string name="step_call_confirm_label">Call confirmation</string>
<string name="step_call_confirm_desc">Ask patient to confirm the call</string>
<string name="step_speaker_choice_label">Speaker choice</string>
<string name="step_speaker_choice_desc">Choose between normal or speakerphone call</string>
<!-- Overlay permission dialog -->
<string name="overlay_permission_title">Permission required</string>
<string name="overlay_permission_text">To show the call overlay, please grant the \"Display over other apps\" permission in settings.</string>
<!-- Contacts screen -->
<string name="dialog_add_contact">Add Contact</string>
<string name="dialog_edit_contact">Edit Contact</string>
<string name="field_name">Name</string>
<string name="field_phone">Phone number</string>
<string name="field_reminder_message">Reminder message (optional)</string>
<!-- Pre-call steps -->
<string name="reminders_step_title">Before calling %1$s</string>
<string name="call_confirm_title">Would you like to call %1$s?</string>
<string name="help_offer_no_guides">No help available.</string>
<string name="guide_no_steps">No steps available.</string>
<string name="guide_step_counter">%1$d / %2$d</string>
<!-- Help config screen -->
<string name="help_config_title">Help Screen</string>
<string name="field_title">Title</string>
<string name="field_subtitle">Subtitle (optional)</string>
<string name="help_config_guides_section">Guides</string>
<string name="dialog_add_guide">Add Guide</string>
<string name="dialog_rename_guide">Rename Guide</string>
<string name="dialog_add_step">Add Step</string>
<string name="dialog_edit_step">Edit Step</string>
<string name="field_text">Text</string>
<!-- Speaker choice and call overlay -->
<string name="speaker_normal_call">Normal call</string>
<string name="speaker_speakerphone">Speakerphone</string>
<string name="overlay_hang_up">Hang up</string>
<string name="cd_toggle_speakerphone">Toggle speakerphone</string>
<!-- Errors and permissions -->
<string name="error_phone_app_not_found">Phone app not found</string>
<string name="error_messages_app_not_found">Messages app not found</string>
<string name="error_camera_app_not_found">Camera app not found</string>
<string name="permission_call_missing">Call permission missing</string>
<!-- Plurals -->
<plurals name="guide_step_count">
<item quantity="one">1 step</item>
<item quantity="other">%d steps</item>
</plurals>
</resources> </resources>
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.IchWillHeim" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.IchWillHeim" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources> </resources>
+1
View File
@@ -3,4 +3,5 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
} }
+4
View File
@@ -1,6 +1,7 @@
[versions] [versions]
agp = "8.13.2" agp = "8.13.2"
kotlin = "2.0.21" kotlin = "2.0.21"
kotlinxSerialization = "1.7.3"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
@@ -15,6 +16,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
@@ -24,9 +26,11 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }