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
+21
View File
@@ -2,8 +2,17 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
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 {
namespace = "com.example.ichwillheim"
compileSdk {
@@ -18,6 +27,9 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "NOCOBASE_TOKEN", "\"$nocobaseToken\"")
buildConfigField("String", "NOCOBASE_URL", "\"$nocobaseUrl\"")
}
buildTypes {
@@ -38,18 +50,27 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
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)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+20
View File
@@ -2,6 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -20,8 +33,15 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".CallOverlayService"
android:foregroundServiceType="specialUse"
android:exported="false" />
</application>
</manifest>
@@ -1,36 +1,51 @@
package com.example.ichwillheim
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
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.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.provider.Settings
import android.telephony.TelephonyManager
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.core.content.ContextCompat
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.fillMaxWidth
import androidx.compose.foundation.layout.height
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.DisposableEffect
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.platform.LocalContext
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.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?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -47,59 +62,217 @@ class MainActivity : ComponentActivity() {
@Composable
fun LauncherScreen(modifier: Modifier = Modifier) {
val context = LocalContext.current
val favorites = listOf(
FavoriteContact(
name = "Sandro",
phone = "+1-555-0100"
),
FavoriteContact(
name = "Paulo",
phone = "+1-555-0111"
)
)
val scope = rememberCoroutineScope()
val apps = listOf(
AppEntry(
label = "Messages",
action = { openMessages(context) }
)
)
var settings by remember { mutableStateOf(SettingsRepository.load(context)) }
Column(
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))
// 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
)
}
@Preview(showBackground = true)
@@ -110,30 +283,23 @@ fun LauncherPreview() {
}
}
@Immutable
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) {
fun openDialerNumber(context: Context, phone: String) {
val intent = Intent(Intent.ACTION_DIAL).apply {
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) {
val intent = Intent(Intent.ACTION_SENDTO).apply {
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) {
@@ -143,3 +309,58 @@ private fun startIntentOrToast(context: Context, intent: Intent, error: String)
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
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val PeachLight = Color(0xFFFFF6E5)
val SageGreen = Color(0xFFA6CDB2)
val WarmOrange = Color(0xFFFCAB7E)
val DarkWarm = Color(0xFF2D2D2D)
val PeachMedium = Color(0xFFE8D4BE)
val PeachSurface = Color(0xFFFEF3E8)
@@ -1,58 +1,34 @@
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.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),
*/
private val ColorScheme = lightColorScheme(
primary = SageGreen,
onPrimary = DarkWarm,
primaryContainer = PeachLight,
onPrimaryContainer = DarkWarm,
secondary = WarmOrange,
onSecondary = DarkWarm,
secondaryContainer = PeachLight,
onSecondaryContainer = DarkWarm,
tertiary = PeachLight,
onTertiary = DarkWarm,
background = PeachLight,
onBackground = DarkWarm,
surface = PeachLight,
onSurface = DarkWarm,
surfaceVariant = PeachSurface,
onSurfaceVariant = DarkWarm,
outline = PeachMedium,
)
@Composable
fun IchWillHeimTheme(
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
}
fun IchWillHeimTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = colorScheme,
colorScheme = ColorScheme,
typography = Typography,
content = content
)
}
}
+114 -1
View File
@@ -1,3 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">IchWillHeim</string>
</resources>
<!-- 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>
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.IchWillHeim" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.IchWillHeim" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources>