diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainActivity.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainActivity.kt index e3a6c1840..8f8059525 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainActivity.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainActivity.kt @@ -3,6 +3,7 @@ package com.steipete.clawdis.node import android.Manifest import android.os.Bundle import android.os.Build +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels @@ -10,7 +11,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.steipete.clawdis.node.ui.RootScreen +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() @@ -21,6 +26,19 @@ class MainActivity : ComponentActivity() { requestNotificationPermissionIfNeeded() NodeForegroundService.start(this) viewModel.camera.attachLifecycleOwner(this) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.preventSleep.collect { enabled -> + if (enabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + } + setContent { MaterialTheme { Surface(modifier = Modifier) { diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt index 527aa470a..bad6c94ea 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt @@ -23,6 +23,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val instanceId: StateFlow = runtime.instanceId val displayName: StateFlow = runtime.displayName val cameraEnabled: StateFlow = runtime.cameraEnabled + val preventSleep: StateFlow = runtime.preventSleep val wakeWords: StateFlow> = runtime.wakeWords val manualEnabled: StateFlow = runtime.manualEnabled val manualHost: StateFlow = runtime.manualHost @@ -44,6 +45,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setCameraEnabled(value) } + fun setPreventSleep(value: Boolean) { + runtime.setPreventSleep(value) + } + fun setManualEnabled(value: Boolean) { runtime.setManualEnabled(value) } diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt index 60e2093be..2e3b0e6fd 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt @@ -78,6 +78,7 @@ class NodeRuntime(context: Context) { val instanceId: StateFlow = prefs.instanceId val displayName: StateFlow = prefs.displayName val cameraEnabled: StateFlow = prefs.cameraEnabled + val preventSleep: StateFlow = prefs.preventSleep val wakeWords: StateFlow> = prefs.wakeWords val manualEnabled: StateFlow = prefs.manualEnabled val manualHost: StateFlow = prefs.manualHost @@ -145,6 +146,10 @@ class NodeRuntime(context: Context) { prefs.setCameraEnabled(value) } + fun setPreventSleep(value: Boolean) { + prefs.setPreventSleep(value) + } + fun setManualEnabled(value: Boolean) { prefs.setManualEnabled(value) } diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt index b6dd875e8..94f1b2fa1 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt @@ -41,6 +41,9 @@ class SecurePrefs(context: Context) { private val _cameraEnabled = MutableStateFlow(prefs.getBoolean("camera.enabled", true)) val cameraEnabled: StateFlow = _cameraEnabled + private val _preventSleep = MutableStateFlow(prefs.getBoolean("screen.preventSleep", true)) + val preventSleep: StateFlow = _preventSleep + private val _manualEnabled = MutableStateFlow(prefs.getBoolean("bridge.manual.enabled", false)) val manualEnabled: StateFlow = _manualEnabled @@ -74,6 +77,11 @@ class SecurePrefs(context: Context) { _cameraEnabled.value = value } + fun setPreventSleep(value: Boolean) { + prefs.edit().putBoolean("screen.preventSleep", value).apply() + _preventSleep.value = value + } + fun setManualEnabled(value: Boolean) { prefs.edit().putBoolean("bridge.manual.enabled", value).apply() _manualEnabled.value = value diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt index 6bfa0b428..699b96299 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt @@ -46,6 +46,7 @@ fun SettingsSheet(viewModel: MainViewModel) { val instanceId by viewModel.instanceId.collectAsState() val displayName by viewModel.displayName.collectAsState() val cameraEnabled by viewModel.cameraEnabled.collectAsState() + val preventSleep by viewModel.preventSleep.collectAsState() val wakeWords by viewModel.wakeWords.collectAsState() val isConnected by viewModel.isConnected.collectAsState() val manualEnabled by viewModel.manualEnabled.collectAsState() @@ -155,6 +156,17 @@ fun SettingsSheet(viewModel: MainViewModel) { item { HorizontalDivider() } + item { Text("Screen") } + item { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) { + Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) + Text(if (preventSleep) "Prevent Sleep" else "Allow Sleep") + } + } + item { Text("Keeps the screen awake while Clawdis is open.") } + + item { HorizontalDivider() } + item { Text("Bridge") } item { Text("Status: $statusText") } item { if (serverName != null) Text("Server: $serverName") } diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 9def87f22..61e5a39ab 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -1,9 +1,12 @@ import SwiftUI +import UIKit struct RootCanvas: View { @Environment(NodeAppModel.self) private var appModel @Environment(VoiceWakeManager.self) private var voiceWake + @Environment(\.scenePhase) private var scenePhase @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false + @AppStorage("screen.preventSleep") private var preventSleep: Bool = true @State private var presentedSheet: PresentedSheet? @State private var voiceWakeToastText: String? @State private var toastDismissTask: Task? @@ -63,6 +66,9 @@ struct RootCanvas: View { } } .preferredColorScheme(.dark) + .onAppear { self.updateIdleTimer() } + .onChange(of: self.preventSleep) { _, _ in self.updateIdleTimer() } + .onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() } .onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in guard let newValue else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) @@ -83,6 +89,7 @@ struct RootCanvas: View { } } .onDisappear { + UIApplication.shared.isIdleTimerDisabled = false self.toastDismissTask?.cancel() self.toastDismissTask = nil } @@ -104,6 +111,10 @@ struct RootCanvas: View { return .disconnected } + + private func updateIdleTimer() { + UIApplication.shared.isIdleTimerDisabled = (self.scenePhase == .active && self.preventSleep) + } } private struct OverlayButton: View { diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index b226db058..8de2be018 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -21,6 +21,7 @@ struct SettingsTab: View { @AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString @AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false @AppStorage("camera.enabled") private var cameraEnabled: Bool = true + @AppStorage("screen.preventSleep") private var preventSleep: Bool = true @AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = "" @AppStorage("bridge.lastDiscoveredStableID") private var lastDiscoveredBridgeStableID: String = "" @AppStorage("bridge.manual.enabled") private var manualBridgeEnabled: Bool = false @@ -73,6 +74,13 @@ struct SettingsTab: View { .foregroundStyle(.secondary) } + Section("Screen") { + Toggle("Prevent Sleep", isOn: self.$preventSleep) + Text("Keeps the screen awake while Clawdis is open.") + .font(.footnote) + .foregroundStyle(.secondary) + } + Section("Bridge") { LabeledContent("Discovery", value: self.bridgeController.discoveryStatusText) LabeledContent("Status", value: self.appModel.bridgeStatusText)